Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/sdroege/gst-plugin-rs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/net
diff options
context:
space:
mode:
authorThibault Saunier <tsaunier@igalia.com>2022-08-16 17:36:53 +0300
committerThibault Saunier <tsaunier@igalia.com>2022-10-18 16:18:53 +0300
commit5e7537953c28ca2d2586f6078c24cccf1c99a27d (patch)
treeaa155f05a0d76f921272b8f331dc24ffb0f0df6b /net
parent020c7e2900ff6b1db9a68c7abae8d282c9e896fb (diff)
webrtc: Move to net/webrtc
Diffstat (limited to 'net')
-rw-r--r--net/webrtc/Cargo.lock2071
-rw-r--r--net/webrtc/Cargo.toml7
-rw-r--r--net/webrtc/LICENSE373
-rw-r--r--net/webrtc/README.md224
-rw-r--r--net/webrtc/plugins/Cargo.toml68
-rw-r--r--net/webrtc/plugins/build.rs3
-rw-r--r--net/webrtc/plugins/examples/README.md18
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats-server.rs235
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/.gitignore4
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/README.md20
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/index.html13
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/package-lock.json919
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/package.json27
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/public/favicon.icobin0 -> 1150 bytes
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/App.svelte182
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/assets/h264.pngbin0 -> 127180 bytes
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/assets/svelte.pngbin0 -> 5185 bytes
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp8.pngbin0 -> 75334 bytes
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp9.pngbin0 -> 71461 bytes
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/components/Consumer.svelte32
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/components/EncoderProps.svelte53
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/components/Header.svelte48
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/components/Modal.svelte50
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/components/PlotConsumerModal.svelte142
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/main.ts7
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/pages/Home.svelte64
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/types/app.ts18
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/src/vite-env.d.ts2
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/svelte.config.js13
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/tsconfig.json24
-rw-r--r--net/webrtc/plugins/examples/webrtcsink-stats/vite.config.js13
-rw-r--r--net/webrtc/plugins/src/gcc/imp.rs1370
-rw-r--r--net/webrtc/plugins/src/gcc/mod.rs16
-rw-r--r--net/webrtc/plugins/src/lib.rs24
-rw-r--r--net/webrtc/plugins/src/signaller/imp.rs478
-rw-r--r--net/webrtc/plugins/src/signaller/mod.rs62
-rw-r--r--net/webrtc/plugins/src/webrtcsink/homegrown_cc.rs420
-rw-r--r--net/webrtc/plugins/src/webrtcsink/imp.rs2852
-rw-r--r--net/webrtc/plugins/src/webrtcsink/mod.rs164
-rw-r--r--net/webrtc/protocol/Cargo.toml12
-rw-r--r--net/webrtc/protocol/src/lib.rs144
-rw-r--r--net/webrtc/signalling/Cargo.toml26
-rw-r--r--net/webrtc/signalling/src/bin/server.rs101
-rw-r--r--net/webrtc/signalling/src/handlers/mod.rs1421
-rw-r--r--net/webrtc/signalling/src/lib.rs2
-rw-r--r--net/webrtc/signalling/src/server/mod.rs218
-rw-r--r--net/webrtc/www/index.html38
-rw-r--r--net/webrtc/www/input.js482
-rw-r--r--net/webrtc/www/keyboard.js3302
-rw-r--r--net/webrtc/www/theme.css141
-rw-r--r--net/webrtc/www/webrtc.js466
51 files changed, 16369 insertions, 0 deletions
diff --git a/net/webrtc/Cargo.lock b/net/webrtc/Cargo.lock
new file mode 100644
index 00000000..a7f6c23f
--- /dev/null
+++ b/net/webrtc/Cargo.lock
@@ -0,0 +1,2071 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
+dependencies = [
+ "autocfg",
+ "concurrent-queue",
+ "futures-lite",
+ "libc",
+ "log",
+ "once_cell",
+ "parking",
+ "polling",
+ "slab",
+ "socket2",
+ "waker-fn",
+ "winapi",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-native-tls"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
+dependencies = [
+ "futures-util",
+ "native-tls",
+ "thiserror",
+ "url",
+]
+
+[[package]]
+name = "async-process"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c"
+dependencies = [
+ "async-io",
+ "autocfg",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "libc",
+ "once_cell",
+ "signal-hook",
+ "winapi",
+]
+
+[[package]]
+name = "async-std"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+dependencies = [
+ "async-attributes",
+ "async-channel",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
+
+[[package]]
+name = "async-tungstenite"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb"
+dependencies = [
+ "async-native-tls",
+ "async-std",
+ "futures-io",
+ "futures-util",
+ "log",
+ "pin-project-lite",
+ "tungstenite",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+
+[[package]]
+name = "atomic_refcell"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+
+[[package]]
+name = "cache-padded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-expr"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "iana-time-zone",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef582e2c00a63a0c0aa1fb4a4870781c4f5729f51196d3537fa7c1c1992eaa3"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
+dependencies = [
+ "cache-padded",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
+
+[[package]]
+name = "futures-lite"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
+
+[[package]]
+name = "futures-task"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
+
+[[package]]
+name = "futures-util"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.16.0"
+source = "git+https://github.com/gtk-rs/gtk-rs-core#bc63686118d9382a5e46260ed040e2028e88321c"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.16.0"
+source = "git+https://github.com/gtk-rs/gtk-rs-core#bc63686118d9382a5e46260ed040e2028e88321c"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.16.0"
+source = "git+https://github.com/gtk-rs/gtk-rs-core#bc63686118d9382a5e46260ed040e2028e88321c"
+dependencies = [
+ "anyhow",
+ "heck",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.16.0"
+source = "git+https://github.com/gtk-rs/gtk-rs-core#bc63686118d9382a5e46260ed040e2028e88321c"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.16.0"
+source = "git+https://github.com/gtk-rs/gtk-rs-core#bc63686118d9382a5e46260ed040e2028e88321c"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gst-plugin-version-helper"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a6a4dd1cb931cc6b49af354a68f21b3aee46b5b07370215d942f3a71542123f"
+dependencies = [
+ "chrono",
+]
+
+[[package]]
+name = "gstreamer"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "glib",
+ "gstreamer-sys",
+ "libc",
+ "muldiv",
+ "num-integer",
+ "num-rational",
+ "once_cell",
+ "option-operations",
+ "paste",
+ "pretty-hex",
+ "serde",
+ "serde_bytes",
+ "thiserror",
+]
+
+[[package]]
+name = "gstreamer-app"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "bitflags",
+ "futures-core",
+ "futures-sink",
+ "glib",
+ "gstreamer",
+ "gstreamer-app-sys",
+ "gstreamer-base",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gstreamer-app-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-base"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "atomic_refcell",
+ "bitflags",
+ "cfg-if",
+ "glib",
+ "gstreamer",
+ "gstreamer-base-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-base-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-rtp"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "bitflags",
+ "glib",
+ "gstreamer",
+ "gstreamer-rtp-sys",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gstreamer-rtp-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-sdp"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib",
+ "gstreamer",
+ "gstreamer-sdp-sys",
+]
+
+[[package]]
+name = "gstreamer-sdp-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-utils"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "gstreamer",
+ "gstreamer-app",
+ "gstreamer-video",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "gstreamer-video"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "futures-channel",
+ "glib",
+ "gstreamer",
+ "gstreamer-base",
+ "gstreamer-video-sys",
+ "libc",
+ "once_cell",
+ "serde",
+]
+
+[[package]]
+name = "gstreamer-video-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-webrtc"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib",
+ "gstreamer",
+ "gstreamer-sdp",
+ "gstreamer-webrtc-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-webrtc-sys"
+version = "0.19.0"
+source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#7721030c15c9692bae262e2d5a4e696a743ec96e"
+dependencies = [
+ "glib-sys",
+ "gstreamer-sdp-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "human_bytes"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a0d4dc39ec942e44c1c306aa196da67f2bd6a30dc7b4a475465c13ccf28817"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.135"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+ "value-bag",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "muldiv"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5136edda114182728ccdedb9f5eda882781f35fa6e80cc360af12a8932507f3"
+
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "openssl"
+version = "0.10.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-operations"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0"
+dependencies = [
+ "paste",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "paste"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "polling"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "libc",
+ "log",
+ "wepoll-ffi",
+ "winapi",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "pretty-hex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
+dependencies = [
+ "once_cell",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[package]]
+name = "security-framework"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "test-log"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f0c854faeb68a048f0f2dc410c5ddae3bf83854ef0e4977d58306a5edef50e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.17.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
+dependencies = [
+ "base64",
+ "byteorder",
+ "bytes",
+ "http",
+ "httparse",
+ "log",
+ "native-tls",
+ "rand",
+ "sha-1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "uuid"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "value-bag"
+version = "1.0.0-alpha.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
+dependencies = [
+ "ctor",
+ "version_check",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webrtcsink"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-native-tls",
+ "async-std",
+ "async-tungstenite",
+ "chrono",
+ "clap",
+ "fastrand",
+ "futures",
+ "gst-plugin-version-helper",
+ "gstreamer",
+ "gstreamer-app",
+ "gstreamer-rtp",
+ "gstreamer-sdp",
+ "gstreamer-utils",
+ "gstreamer-video",
+ "gstreamer-webrtc",
+ "human_bytes",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "smallvec",
+ "thiserror",
+ "tracing",
+ "tracing-log",
+ "tracing-subscriber",
+ "uuid",
+ "webrtcsink-protocol",
+]
+
+[[package]]
+name = "webrtcsink-protocol"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "webrtcsink-signalling"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-native-tls",
+ "async-std",
+ "async-tungstenite",
+ "clap",
+ "futures",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "test-log",
+ "thiserror",
+ "tracing",
+ "tracing-log",
+ "tracing-subscriber",
+ "uuid",
+ "webrtcsink-protocol",
+]
+
+[[package]]
+name = "wepoll-ffi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
diff --git a/net/webrtc/Cargo.toml b/net/webrtc/Cargo.toml
new file mode 100644
index 00000000..b8454061
--- /dev/null
+++ b/net/webrtc/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+
+members = [
+ "plugins",
+ "protocol",
+ "signalling",
+]
diff --git a/net/webrtc/LICENSE b/net/webrtc/LICENSE
new file mode 100644
index 00000000..14e2f777
--- /dev/null
+++ b/net/webrtc/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/net/webrtc/README.md b/net/webrtc/README.md
new file mode 100644
index 00000000..5380f260
--- /dev/null
+++ b/net/webrtc/README.md
@@ -0,0 +1,224 @@
+# webrtcsink
+
+All-batteries included GStreamer WebRTC producer, that tries its best to do The Right Thingâ„¢.
+
+## Use case
+
+The [webrtcbin] element in GStreamer is extremely flexible and powerful, but using
+it can be a difficult exercise. When all you want to do is serve a fixed set of streams
+to any number of consumers, `webrtcsink` (which wraps `webrtcbin` internally) can be a
+useful alternative.
+
+[webrtcbin]: https://gstreamer.freedesktop.org/documentation/webrtc/index.html
+
+## Features
+
+`webrtcsink` implements the following features:
+
+* Built-in signaller: when using the default signalling server, this element will
+ perform signalling without requiring application interaction.
+ This makes it usable directly from `gst-launch`.
+
+* Application-provided signalling: `webrtcsink` can be instantiated by an application
+ with a custom signaller. That signaller must be a GObject, and must implement the
+ `Signallable` interface as defined [here](plugins/src/webrtcsink/mod.rs). The
+ [default signaller](plugins/src/signaller/mod.rs) can be used as an example.
+
+ An [example project] is also available to use as a boilerplate for implementing
+ and using a custom signaller.
+
+* Sandboxed consumers: when a consumer is added, its encoder / payloader / webrtcbin
+ elements run in a separately managed pipeline. This provides a certain level of
+ sandboxing, as opposed to having those elements running inside the element itself.
+
+ It is important to note that at this moment, encoding is not shared between consumers.
+ While this is not on the roadmap at the moment, nothing in the design prevents
+ implementing this optimization.
+
+* Congestion control: the element leverages transport-wide congestion control
+ feedback messages in order to adapt the bitrate of individual consumers' video
+ encoders to the available bandwidth.
+
+* Configuration: the level of user control over the element is slowly expanding,
+ consult `gst-inspect-1.0` for more information on the available properties and
+ signals.
+
+* Packet loss mitigation: webrtcsink now supports sending protection packets for
+ Forward Error Correction, modulating the amount as a function of the available
+ bandwidth, and can honor retransmission requests. Both features can be disabled
+ via properties.
+
+It is important to note that full control over the individual elements used by
+`webrtcsink` is *not* on the roadmap, as it will act as a black box in that respect,
+for example `webrtcsink` wants to reserve control over the bitrate for congestion
+control.
+
+A signal is now available however for the application to provide the initial
+configuration for the encoders `webrtcsink` instantiates.
+
+If more granular control is required, applications should use `webrtcbin` directly,
+`webrtcsink` will focus on trying to just do the right thing, although it might
+expose more interfaces to guide and tune the heuristics it employs.
+
+[example project]: https://github.com/centricular/webrtcsink-custom-signaller
+
+## Building
+
+### Prerequisites
+
+The element has only been tested for now against GStreamer main.
+
+For testing, it is recommended to simply build GStreamer locally and run
+in the uninstalled devenv.
+
+> Make sure to install the development packages for some codec libraries
+> beforehand, such as libx264, libvpx and libopusenc, exact names depend
+> on your distribution.
+
+```
+git clone --depth 1 --single-branch --branch main https://gitlab.freedesktop.org/gstreamer/gstreamer
+cd gstreamer
+meson build
+ninja -C build
+ninja -C build devenv
+```
+
+### Compiling
+
+``` shell
+cargo build
+```
+
+## Usage
+
+Open three terminals. In the first, run:
+
+``` shell
+WEBRTCSINK_SIGNALLING_SERVER_LOG=debug cargo run --bin server
+```
+
+In the second, run:
+
+``` shell
+python3 -m http.server -d www/
+```
+
+In the third, run:
+
+``` shell
+export GST_PLUGIN_PATH=$PWD/target/debug:$GST_PLUGIN_PATH
+gst-launch-1.0 webrtcsink name=ws videotestsrc ! ws. audiotestsrc ! ws.
+```
+
+When the pipeline above is running succesfully, open a browser and
+point it to the http server:
+
+``` shell
+xdg-open http://127.0.0.1:8000
+```
+
+You should see an identifier listed in the left-hand panel, click on
+it. You should see a test video stream, and hear a test tone.
+
+## Configuration
+
+The element itself can be configured through its properties, see
+`gst-inspect-1.0 webrtcsink` for more information about that, in addition the
+default signaller also exposes properties for configuring it, in
+particular setting the signalling server address, those properties
+can be accessed through the `gst::ChildProxy` interface, for example
+with gst-launch:
+
+``` shell
+gst-launch-1.0 webrtcsink signaller::address="ws://127.0.0.1:8443" ..
+```
+
+The signaller object can not be inspected, refer to [the source code]
+for the list of properties.
+
+[the source code]: plugins/src/signaller/imp.rs
+
+
+### Enable 'navigation' a.k.a user interactivity with the content
+
+`webrtcsink` implements the [`GstNavigation`] interface which allows interacting
+with the content, for example move with your mouse, entering keys with the
+keyboard, etc... On top of that a `WebRTCDataChannel` based protocol has been
+implemented and can be activated with the `enable-data-channel-navigation=true`
+property. The [demo](www/) implements the protocol and you can easily test this
+feature, using the [`wpesrc`] for example.
+
+As an example, the following pipeline allows you to navigate the GStreamer
+documentation inside the video running within your web browser (in
+http://127.0.0.1:8000 if you followed previous steps of that readme):
+
+```
+gst-launch-1.0 wpesrc location=https://gstreamer.freedesktop.org/documentation/ ! webrtcsink enable-data-channel-navigation=true
+```
+
+[`GstNavigation`]: https://gstreamer.freedesktop.org/documentation/video/gstnavigation.html
+[`wpesrc`]: https://gstreamer.freedesktop.org/documentation/wpe/wpesrc.html
+
+## Testing congestion control
+
+For the purpose of testing congestion in a reproducible manner, a
+[simple tool] has been used, I only used it on Linux but it is documented
+as usable on MacOS too. I had to run the client browser on a separate
+machine on my local network for congestion to actually be applied, I didn't
+look into why that was necessary.
+
+My testing procedure was:
+
+* identify the server machine network interface (eg with `ifconfig` on Linux)
+
+* identify the client machine IP address (eg with `ifconfig` on Linux)
+
+* start the various services as explained in the Usage section (use
+ `GST_DEBUG=webrtcsink:7` to get detailed logs about congestion control)
+
+* start playback in the client browser
+
+* Run a `comcast` command on the server machine, for instance:
+
+ ``` shell
+ /home/meh/go/bin/comcast --device=$SERVER_INTERFACE --target-bw 3000 --target-addr=$CLIENT_IP --target-port=1:65535 --target-proto=udp
+ ```
+
+* Observe the bitrate sharply decreasing, playback should slow down briefly
+ then catch back up
+
+* Remove the bandwidth limitation, and observe the bitrate eventually increasing
+ back to a maximum:
+
+ ``` shell
+ /home/meh/go/bin/comcast --device=$SERVER_INTERFACE --stop
+ ```
+
+For comparison, the congestion control property can be set to disabled on
+webrtcsink, then the above procedure applied again, the expected result is
+for playback to simply crawl down to a halt until the bandwidth limitation
+is lifted:
+
+``` shell
+gst-launch-1.0 webrtcsink congestion-control=disabled
+```
+
+[simple tool]: https://github.com/tylertreat/comcast
+
+## Monitoring tool
+
+An example server / client application for monitoring per-consumer stats
+can be found [here].
+
+[here]: plugins/examples/README.md
+
+## License
+
+All the rust code in this repository is licensed under the [Mozilla Public License Version 2.0].
+
+Parts of the JavaScript code in the www/ example are licensed under the [Apache License, Version 2.0],
+the rest is licensed under the [Mozilla Public License Version 2.0] unless advertised in the
+header.
+
+[Mozilla Public License Version 2.0]: http://opensource.org/licenses/MPL-2.0
+[Apache License, Version 2.0]: https://www.apache.org/licenses/LICENSE-2.1
diff --git a/net/webrtc/plugins/Cargo.toml b/net/webrtc/plugins/Cargo.toml
new file mode 100644
index 00000000..51debdda
--- /dev/null
+++ b/net/webrtc/plugins/Cargo.toml
@@ -0,0 +1,68 @@
+[package]
+name = "webrtcsink"
+version = "0.1.0"
+edition = "2018"
+authors = ["Mathieu Duponchelle <mathieu@centricular.com>"]
+license = "MPL-2.0"
+description = "GStreamer WebRTC sink"
+repository = "https://github.com/centricular/webrtcsink/"
+build = "build.rs"
+
+[dependencies]
+gst = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer", features = ["v1_20", "serde"] }
+gst-app = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-app", features = ["v1_20"] }
+gst-video = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-video", features = ["v1_20", "serde"] }
+gst-webrtc = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-webrtc", features = ["v1_20"] }
+gst-sdp = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-sdp", features = ["v1_20"] }
+gst-rtp = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-rtp", features = ["v1_20"] }
+gst-utils = { git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", package = "gstreamer-utils" }
+once_cell = "1.0"
+chrono = { version = "0.4", default-features = false }
+smallvec = "1"
+anyhow = "1"
+thiserror = "1"
+futures = "0.3"
+async-std = { version = "1", features = ["unstable"] }
+async-native-tls = { version = "0.4.0" }
+async-tungstenite = { version = "0.17", features = ["async-std-runtime", "async-native-tls"] }
+serde = "1"
+serde_json = "1"
+fastrand = "1.0"
+webrtcsink-protocol = { version = "0.1", path="../protocol" }
+human_bytes = "0.3.1"
+
+[dev-dependencies]
+tracing = { version = "0.1", features = ["log"] }
+tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
+tracing-log = "0.1"
+uuid = { version = "1", features = ["v4"] }
+clap = { version = "4", features = ["derive"] }
+
+[lib]
+name = "webrtcsink"
+crate-type = ["cdylib", "rlib"]
+path = "src/lib.rs"
+
+[build-dependencies]
+gst-plugin-version-helper = "0.7"
+
+[features]
+static = []
+capi = []
+gst1_22 = ["gst/v1_22", "gst-app/v1_22", "gst-video/v1_22", "gst-webrtc/v1_22", "gst-sdp/v1_22", "gst-rtp/v1_22"]
+
+[package.metadata.capi]
+min_version = "0.8.0"
+
+[package.metadata.capi.header]
+enabled = false
+
+[package.metadata.capi.library]
+install_subdir = "gstreamer-1.0"
+versioning = false
+
+[package.metadata.capi.pkg_config]
+requires_private = "gstreamer-rtp >= 1.20, gstreamer-webrtc >= 1.20, gstreamer-1.0 >= 1.20, gstreamer-app >= 1.20, gstreamer-video >= 1.20, gstreamer-sdp >= 1.20, gobject-2.0, glib-2.0, gmodule-2.0"
+
+[[example]]
+name = "webrtcsink-stats-server"
diff --git a/net/webrtc/plugins/build.rs b/net/webrtc/plugins/build.rs
new file mode 100644
index 00000000..cda12e57
--- /dev/null
+++ b/net/webrtc/plugins/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ gst_plugin_version_helper::info()
+}
diff --git a/net/webrtc/plugins/examples/README.md b/net/webrtc/plugins/examples/README.md
new file mode 100644
index 00000000..7b402e69
--- /dev/null
+++ b/net/webrtc/plugins/examples/README.md
@@ -0,0 +1,18 @@
+# webrtcsink examples
+
+Collection (1-sized for now) of webrtcsink examples
+
+## webrtcsink-stats-server
+
+A simple application that instantiates a webrtcsink and serves stats
+over websockets.
+
+The application expects a signalling server to be running at `ws://localhost:8443`,
+similar to the usage example in the main README.
+
+``` shell
+cargo run --example webrtcsink-stats-server
+```
+
+Once it is running, follow the instruction in the webrtcsink-stats folder to
+run an example client.
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats-server.rs b/net/webrtc/plugins/examples/webrtcsink-stats-server.rs
new file mode 100644
index 00000000..6088dba4
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats-server.rs
@@ -0,0 +1,235 @@
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+use anyhow::Error;
+
+use async_std::net::{TcpListener, TcpStream};
+use async_std::task;
+use async_tungstenite::tungstenite::Message as WsMessage;
+use clap::Parser;
+use futures::channel::mpsc;
+use futures::prelude::*;
+use gst::glib::Type;
+use gst::prelude::*;
+use tracing::{debug, info, trace};
+use tracing_subscriber::prelude::*;
+
+#[derive(Parser, Debug)]
+#[clap(about, version, author)]
+/// Program arguments
+struct Args {
+ /// URI of file to serve. Must hold at least one audio and video stream
+ uri: String,
+ /// Disable Forward Error Correction
+ #[clap(long)]
+ disable_fec: bool,
+ /// Disable retransmission
+ #[clap(long)]
+ disable_retransmission: bool,
+ /// Disable congestion control
+ #[clap(long)]
+ disable_congestion_control: bool,
+}
+
+fn serialize_value(val: &gst::glib::Value) -> Option<serde_json::Value> {
+ match val.type_() {
+ Type::STRING => Some(val.get::<String>().unwrap().into()),
+ Type::BOOL => Some(val.get::<bool>().unwrap().into()),
+ Type::I32 => Some(val.get::<i32>().unwrap().into()),
+ Type::U32 => Some(val.get::<u32>().unwrap().into()),
+ Type::I_LONG | Type::I64 => Some(val.get::<i64>().unwrap().into()),
+ Type::U_LONG | Type::U64 => Some(val.get::<u64>().unwrap().into()),
+ Type::F32 => Some(val.get::<f32>().unwrap().into()),
+ Type::F64 => Some(val.get::<f64>().unwrap().into()),
+ _ => {
+ if let Ok(s) = val.get::<gst::Structure>() {
+ serde_json::to_value(
+ s.iter()
+ .filter_map(|(name, value)| {
+ serialize_value(value).map(|value| (name.to_string(), value))
+ })
+ .collect::<HashMap<String, serde_json::Value>>(),
+ )
+ .ok()
+ } else if let Ok(a) = val.get::<gst::Array>() {
+ serde_json::to_value(
+ a.iter()
+ .filter_map(|value| serialize_value(value))
+ .collect::<Vec<serde_json::Value>>(),
+ )
+ .ok()
+ } else if let Some((_klass, values)) = gst::glib::FlagsValue::from_value(val) {
+ Some(
+ values
+ .iter()
+ .map(|value| value.nick())
+ .collect::<Vec<&str>>()
+ .join("+")
+ .into(),
+ )
+ } else if let Ok(value) = val.serialize() {
+ Some(value.as_str().into())
+ } else {
+ None
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+struct Listener {
+ id: uuid::Uuid,
+ sender: mpsc::Sender<WsMessage>,
+}
+
+struct State {
+ listeners: Vec<Listener>,
+}
+
+async fn run(args: Args) -> Result<(), Error> {
+ tracing_log::LogTracer::init().expect("Failed to set logger");
+ let env_filter = tracing_subscriber::EnvFilter::try_from_env("WEBRTCSINK_STATS_LOG")
+ .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
+ let fmt_layer = tracing_subscriber::fmt::layer()
+ .with_thread_ids(true)
+ .with_target(true)
+ .with_span_events(
+ tracing_subscriber::fmt::format::FmtSpan::NEW
+ | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
+ );
+ let subscriber = tracing_subscriber::Registry::default()
+ .with(env_filter)
+ .with(fmt_layer);
+ tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
+
+ let state = Arc::new(Mutex::new(State { listeners: vec![] }));
+
+ let addr = "127.0.0.1:8484".to_string();
+
+ // Create the event loop and TCP listener we'll accept connections on.
+ let try_socket = TcpListener::bind(&addr).await;
+ let listener = try_socket.expect("Failed to bind");
+ info!("Listening on: {}", addr);
+
+ let pipeline_str = format!(
+ "webrtcsink name=ws do-retransmission={} do-fec={} congestion-control={} \
+ uridecodebin name=d uri={} \
+ d. ! video/x-raw ! queue ! ws.video_0 \
+ d. ! audio/x-raw ! queue ! ws.audio_0",
+ !args.disable_retransmission,
+ !args.disable_fec,
+ if args.disable_congestion_control {
+ "disabled"
+ } else {
+ "homegrown"
+ },
+ args.uri
+ );
+
+ let pipeline = gst::parse_launch(&pipeline_str)?;
+ let ws = pipeline
+ .downcast_ref::<gst::Bin>()
+ .unwrap()
+ .by_name("ws")
+ .unwrap();
+
+ ws.connect("encoder-setup", false, |values| {
+ let encoder = values[3].get::<gst::Element>().unwrap();
+
+ info!("Encoder: {}", encoder.factory().unwrap().name());
+
+ let configured = if let Some(factory) = encoder.factory() {
+ match factory.name().as_str() {
+ "does-not-exist" => {
+ // One could configure a hardware encoder to their liking here,
+ // and return true to make sure webrtcsink does not do any configuration
+ // of its own
+ true
+ }
+ _ => false,
+ }
+ } else {
+ false
+ };
+
+ Some(configured.to_value())
+ });
+
+ let ws_clone = ws.downgrade();
+ let state_clone = state.clone();
+ task::spawn(async move {
+ let mut interval = async_std::stream::interval(std::time::Duration::from_millis(100));
+
+ while interval.next().await.is_some() {
+ if let Some(ws) = ws_clone.upgrade() {
+ let stats = ws.property::<gst::Structure>("stats");
+ let stats = serialize_value(&stats.to_value()).unwrap();
+ debug!("Stats: {}", serde_json::to_string_pretty(&stats).unwrap());
+ let msg = WsMessage::Text(serde_json::to_string(&stats).unwrap());
+
+ let listeners = state_clone.lock().unwrap().listeners.clone();
+
+ for mut listener in listeners {
+ if listener.sender.send(msg.clone()).await.is_err() {
+ let mut state = state_clone.lock().unwrap();
+ let index = state
+ .listeners
+ .iter()
+ .position(|l| l.id == listener.id)
+ .unwrap();
+ state.listeners.remove(index);
+ }
+ }
+ } else {
+ break;
+ }
+ }
+ });
+
+ pipeline.set_state(gst::State::Playing)?;
+
+ while let Ok((stream, _)) = listener.accept().await {
+ task::spawn(accept_connection(state.clone(), stream));
+ }
+
+ Ok(())
+}
+
+async fn accept_connection(state: Arc<Mutex<State>>, stream: TcpStream) {
+ let addr = stream
+ .peer_addr()
+ .expect("connected streams should have a peer address");
+ info!("Peer address: {}", addr);
+
+ let mut ws_stream = async_tungstenite::accept_async(stream)
+ .await
+ .expect("Error during the websocket handshake occurred");
+
+ info!("New WebSocket connection: {}", addr);
+
+ let mut state = state.lock().unwrap();
+ let (sender, mut receiver) = mpsc::channel::<WsMessage>(1000);
+ state.listeners.push(Listener {
+ id: uuid::Uuid::new_v4(),
+ sender,
+ });
+ drop(state);
+
+ task::spawn(async move {
+ while let Some(msg) = receiver.next().await {
+ trace!("Sending to one listener!");
+ if ws_stream.send(msg).await.is_err() {
+ info!("Listener errored out");
+ receiver.close();
+ }
+ }
+ });
+}
+
+fn main() -> Result<(), Error> {
+ gst::init()?;
+
+ let args = Args::parse();
+
+ task::block_on(run(args))
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/.gitignore b/net/webrtc/plugins/examples/webrtcsink-stats/.gitignore
new file mode 100644
index 00000000..126fe84d
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/.gitignore
@@ -0,0 +1,4 @@
+/node_modules/
+/dist/
+/.vscode/
+.DS_Store
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/README.md b/net/webrtc/plugins/examples/webrtcsink-stats/README.md
new file mode 100644
index 00000000..e1c779bc
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/README.md
@@ -0,0 +1,20 @@
+# Example web client for webrtcsink-stats-server
+
+This web client will display live statistics as received through a
+websocket connected to a `webrtcsink-stats-server`.
+
+Usage:
+
+``` shell
+npm install
+npm run dev
+```
+
+Then navigate to `http://localhost:3000/`. Once consumers are connected
+to the webrtc-sink-stats-server, they should be listed on the page, clicking
+on any consumer will show a modal with plots for some of the most interesting
+statistics.
+
+The stat server can also be specified through the `remote-url` search parameter,
+for example you can access a distant stat server with
+`http://localhost:3000?remote-uri=my-remoye.com:72522`.
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/index.html b/net/webrtc/plugins/examples/webrtcsink-stats/index.html
new file mode 100644
index 00000000..d2f6839b
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" href="/favicon.ico" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Svelte + TS + Vite App</title>
+ </head>
+ <body>
+ <div id="app"></div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/package-lock.json b/net/webrtc/plugins/examples/webrtcsink-stats/package-lock.json
new file mode 100644
index 00000000..03d18f6d
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/package-lock.json
@@ -0,0 +1,919 @@
+{
+ "name": "webrtcsink-stats",
+ "version": "0.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": {
+ "version": "0.2.36",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
+ "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
+ "dev": true
+ },
+ "@fortawesome/free-solid-svg-icons": {
+ "version": "5.15.4",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
+ "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
+ "dev": true,
+ "requires": {
+ "@fortawesome/fontawesome-common-types": "^0.2.36"
+ }
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@rollup/pluginutils": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz",
+ "integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==",
+ "dev": true,
+ "requires": {
+ "estree-walker": "^2.0.1",
+ "picomatch": "^2.2.2"
+ }
+ },
+ "@sveltejs/vite-plugin-svelte": {
+ "version": "1.0.0-next.30",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.30.tgz",
+ "integrity": "sha512-YQqdMxjL1VgSFk4/+IY3yLwuRRapPafPiZTiaGEq1psbJYSNYUWx9F1zMm32GMsnogg3zn99mGJOqe3ld3HZSg==",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^4.1.1",
+ "debug": "^4.3.2",
+ "kleur": "^4.1.4",
+ "magic-string": "^0.25.7",
+ "require-relative": "^0.8.7",
+ "svelte-hmr": "^0.14.7"
+ }
+ },
+ "@tsconfig/svelte": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-2.0.1.tgz",
+ "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "16.11.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz",
+ "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==",
+ "dev": true
+ },
+ "@types/pug": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.5.tgz",
+ "integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==",
+ "dev": true
+ },
+ "@types/sass": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz",
+ "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+ "dev": true
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
+ "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "detect-indent": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
+ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
+ "dev": true
+ },
+ "es6-promise": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
+ "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=",
+ "dev": true
+ },
+ "esbuild": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
+ "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
+ "dev": true,
+ "requires": {
+ "esbuild-android-arm64": "0.13.15",
+ "esbuild-darwin-64": "0.13.15",
+ "esbuild-darwin-arm64": "0.13.15",
+ "esbuild-freebsd-64": "0.13.15",
+ "esbuild-freebsd-arm64": "0.13.15",
+ "esbuild-linux-32": "0.13.15",
+ "esbuild-linux-64": "0.13.15",
+ "esbuild-linux-arm": "0.13.15",
+ "esbuild-linux-arm64": "0.13.15",
+ "esbuild-linux-mips64le": "0.13.15",
+ "esbuild-linux-ppc64le": "0.13.15",
+ "esbuild-netbsd-64": "0.13.15",
+ "esbuild-openbsd-64": "0.13.15",
+ "esbuild-sunos-64": "0.13.15",
+ "esbuild-windows-32": "0.13.15",
+ "esbuild-windows-64": "0.13.15",
+ "esbuild-windows-arm64": "0.13.15"
+ }
+ },
+ "esbuild-android-arm64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz",
+ "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz",
+ "integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-arm64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz",
+ "integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz",
+ "integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-arm64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz",
+ "integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-32": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz",
+ "integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz",
+ "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz",
+ "integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz",
+ "integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-mips64le": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz",
+ "integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-ppc64le": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz",
+ "integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-netbsd-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz",
+ "integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-openbsd-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz",
+ "integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-sunos-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz",
+ "integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-32": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz",
+ "integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz",
+ "integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-arm64": {
+ "version": "0.13.15",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz",
+ "integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==",
+ "dev": true,
+ "optional": true
+ },
+ "estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+ "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ }
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
+ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
+ "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "kleur": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
+ "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
+ "dev": true
+ },
+ "magic-string": {
+ "version": "0.25.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
+ "min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "moment": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+ },
+ "mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "nanoid": {
+ "version": "3.1.30",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
+ "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
+ "dev": true
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
+ "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.4.4",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz",
+ "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.1.30",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.1"
+ }
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-relative": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
+ "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "rollup": {
+ "version": "2.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.1.tgz",
+ "integrity": "sha512-akwfnpjY0rXEDSn1UTVfKXJhPsEBu+imi1gqBA1ZkHGydUnkV/fWCC90P7rDaLEW8KTwBcS1G3N4893Ndz+jwg==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "sade": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
+ "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
+ "dev": true,
+ "requires": {
+ "mri": "^1.1.0"
+ }
+ },
+ "sander": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
+ "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=",
+ "dev": true,
+ "requires": {
+ "es6-promise": "^3.1.2",
+ "graceful-fs": "^4.1.3",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.2"
+ }
+ },
+ "sass": {
+ "version": "1.43.5",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.5.tgz",
+ "integrity": "sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==",
+ "dev": true,
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0"
+ }
+ },
+ "sorcery": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
+ "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=",
+ "dev": true,
+ "requires": {
+ "buffer-crc32": "^0.2.5",
+ "minimist": "^1.2.0",
+ "sander": "^0.5.0",
+ "sourcemap-codec": "^1.3.0"
+ }
+ },
+ "source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+ "dev": true
+ },
+ "source-map-js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz",
+ "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==",
+ "dev": true
+ },
+ "sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "dev": true
+ },
+ "strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "requires": {
+ "min-indent": "^1.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "svelte": {
+ "version": "3.44.2",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.2.tgz",
+ "integrity": "sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA==",
+ "dev": true
+ },
+ "svelte-check": {
+ "version": "2.2.10",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.2.10.tgz",
+ "integrity": "sha512-UVLd/N7hUIG2v6dytofsw8MxYn2iS2hpNSglsGz9Z9b8ZfbJ5jayl4Mm1SXhNwiFs5aklG90zSBJtd7NTK8dTg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "chokidar": "^3.4.1",
+ "fast-glob": "^3.2.7",
+ "import-fresh": "^3.2.1",
+ "minimist": "^1.2.5",
+ "sade": "^1.7.4",
+ "source-map": "^0.7.3",
+ "svelte-preprocess": "^4.0.0",
+ "typescript": "*"
+ }
+ },
+ "svelte-fa": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/svelte-fa/-/svelte-fa-2.4.0.tgz",
+ "integrity": "sha512-0bnbMGbsE1LUnlioDcf27tl2O8kjuXlTXMXzIxC7LoIOWmqn0D+zd539HfLiQbdLuOHGTaynwN9V+4ehhEu1Jw==",
+ "dev": true
+ },
+ "svelte-hmr": {
+ "version": "0.14.7",
+ "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.14.7.tgz",
+ "integrity": "sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==",
+ "dev": true
+ },
+ "svelte-preprocess": {
+ "version": "4.9.8",
+ "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz",
+ "integrity": "sha512-EQS/oRZzMtYdAprppZxY3HcysKh11w54MgA63ybtL+TAZ4hVqYOnhw41JVJjWN9dhPnNjjLzvbZ2tMhTsla1Og==",
+ "dev": true,
+ "requires": {
+ "@types/pug": "^2.0.4",
+ "@types/sass": "^1.16.0",
+ "detect-indent": "^6.0.0",
+ "magic-string": "^0.25.7",
+ "sorcery": "^0.10.0",
+ "strip-indent": "^3.0.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ },
+ "typescript": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
+ "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
+ "dev": true
+ },
+ "vite": {
+ "version": "2.6.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.14.tgz",
+ "integrity": "sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.13.2",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.3.8",
+ "resolve": "^1.20.0",
+ "rollup": "^2.57.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ }
+ }
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/package.json b/net/webrtc/plugins/examples/webrtcsink-stats/package.json
new file mode 100644
index 00000000..b0f3947e
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "webrtcsink-stats",
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview",
+ "check": "svelte-check --tsconfig ./tsconfig.json"
+ },
+ "devDependencies": {
+ "@fortawesome/free-solid-svg-icons": "^5.15.4",
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.11",
+ "@tsconfig/svelte": "^2.0.1",
+ "sass": "^1.43.5",
+ "svelte": "^3.37.0",
+ "svelte-check": "^2.1.0",
+ "svelte-fa": "^2.4.0",
+ "svelte-preprocess": "^4.7.2",
+ "tslib": "^2.2.0",
+ "typescript": "^4.3.2",
+ "vite": "^2.6.4"
+ },
+ "dependencies": {
+ "moment": "^2.29.1"
+ }
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/public/favicon.ico b/net/webrtc/plugins/examples/webrtcsink-stats/public/favicon.ico
new file mode 100644
index 00000000..d75d248e
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/public/favicon.ico
Binary files differ
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/App.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/App.svelte
new file mode 100644
index 00000000..48fcc727
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/App.svelte
@@ -0,0 +1,182 @@
+<svelte:head>
+ <script src="https://cdn.plot.ly/plotly-latest.min.js" type="text/javascript"></script>
+</svelte:head>
+
+<script lang="ts">
+ import Home from '@/pages/Home.svelte'
+ import Header from '@/components/Header.svelte'
+ import type { ConsumerType } from '@/types/app'
+ import { WebSocketStatus, MitigationMode } from '@/types/app'
+ import { onMount, onDestroy } from 'svelte';
+
+ let ws: WebSocket | undefined = undefined
+ let websocketStatus: WebSocketStatus = WebSocketStatus.Connecting
+ let consumers: Map<string, ConsumerType> = new Map ()
+ let consumers_array: Array<ConsumerType> = []
+ let timeout: ReturnType<typeof setTimeout> | undefined = undefined
+
+ const updateConsumerStats = (consumer: ConsumerType, stats: Object) => {
+ let target_bitrate = 0
+ let fec_percentage = 0
+ let keyframe_requests = 0
+ let retransmission_requests = 0
+ let bitrate_sent = 0
+ let bitrate_recv = 0
+ let packet_loss = 0
+ let delta_of_delta = 0
+
+ if (stats["consumer-stats"]["video-encoders"].length > 0) {
+ let venc = stats["consumer-stats"]["video-encoders"][0]
+ target_bitrate = venc["bitrate"]
+ fec_percentage = venc["fec-percentage"]
+ consumer.video_codec = venc["codec-name"]
+
+ let mitigation_mode = MitigationMode.None
+
+ for (let mode of venc["mitigation-mode"].split("+")) {
+ switch (mode) {
+ case "none": {
+ mitigation_mode |= MitigationMode.None
+ break
+ }
+ case "downscaled": {
+ mitigation_mode |= MitigationMode.Downscaled
+ break
+ }
+ case "downsampled": {
+ mitigation_mode |= MitigationMode.Downsampled
+ break
+ }
+ }
+ }
+
+ consumer.mitigation_mode = mitigation_mode
+ }
+
+
+ for (let svalue of Object.values(stats)) {
+ if (svalue["type"] == "transport") {
+ let twcc_stats = svalue["gst-twcc-stats"]
+ if (twcc_stats !== undefined) {
+ bitrate_sent = twcc_stats["bitrate-sent"]
+ bitrate_recv = twcc_stats["bitrate-recv"]
+ packet_loss = twcc_stats["packet-loss-pct"]
+ delta_of_delta = twcc_stats["avg-delta-of-delta"]
+ }
+ } else if (svalue["type"] == "outbound-rtp") {
+ keyframe_requests += svalue["pli-count"]
+ retransmission_requests += svalue["nack-count"]
+ }
+ }
+
+ consumer.stats["target_bitrate"] = target_bitrate
+ consumer.stats["fec_percentage"] = fec_percentage
+ consumer.stats["bitrate_sent"] = bitrate_sent
+ consumer.stats["bitrate_recv"] = bitrate_recv
+ consumer.stats["packet_loss"] = packet_loss
+ consumer.stats["delta_of_delta"] = delta_of_delta
+ consumer.stats["keyframe_requests"] = keyframe_requests
+ consumer.stats["retransmission_requests"] = retransmission_requests
+ }
+
+ const fetchStats = () => {
+ const urlParams = new URLSearchParams(window.location.search);
+ var remote_server = urlParams.get('remote-url');
+ if (!remote_server)
+ remote_server = "127.0.0.1:8484"
+ const ws_url = `ws://${remote_server}`;
+
+ console.info(`Logging to ${ws_url}`);
+ ws = new WebSocket(ws_url);
+
+ ws.onerror = () => {
+ websocketStatus = WebSocketStatus.Error
+ }
+
+ ws.onclose = () => {
+ websocketStatus = WebSocketStatus.Error
+ consumers = new Map()
+ consumers_array = []
+ timeout = setTimeout(fetchStats, 500)
+ }
+
+ ws.onopen = () => {
+ websocketStatus = WebSocketStatus.Connected
+ }
+
+ ws.onmessage = (event) => {
+ let stats = JSON.parse(event.data)
+ // Set is supposed to be buildable from an iterator,
+ // no idea why the Arra.from is needed ..
+ let to_remove = new Set(Array.from(consumers.keys()))
+
+ for (let [key, value] of Object.entries(stats)) {
+ let consumer = undefined;
+
+ if (consumers.get(key) === undefined) {
+ consumer = {
+ id: key,
+ video_codec: undefined,
+ mitigation_mode: MitigationMode.None,
+ stats: new Map([
+ ["target_bitrate", 0],
+ ["fec_percentage", 0],
+ ["bitrate_sent", 0],
+ ["bitrate_recv", 0],
+ ["packet_loss", 0],
+ ["delta_of_delta", 0],
+ ["keyframe_requests", 0],
+ ["retransmission_requests", 0],
+ ]),
+ }
+ consumers.set(key, consumer)
+ } else {
+ consumer = consumers.get(key)
+ }
+
+ updateConsumerStats(consumer, value)
+
+ to_remove.delete(key)
+ }
+
+ for (let key of to_remove) {
+ consumers.delete(key)
+ }
+
+ consumers_array = Array.from(consumers.values())
+ }
+ }
+
+ const closeWebSocket = () => {
+ if (ws != undefined) {
+ ws.close();
+ ws = undefined;
+ }
+
+ if (timeout != undefined) {
+ clearTimeout(timeout)
+ timeout = undefined
+ }
+ }
+
+ onMount(fetchStats)
+ onDestroy(closeWebSocket)
+</script>
+
+<Header websocketStatus={ websocketStatus } />
+
+<Home consumers={ consumers_array } />
+
+<style lang="scss">
+ :root {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
+ Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ height: 100%;
+ }
+ :global(body) {
+ /* this will apply to <body> */
+ margin: 0;
+ height: 100%;
+ background-color: #fbfbfb;
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/h264.png b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/h264.png
new file mode 100644
index 00000000..ab0cd852
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/h264.png
Binary files differ
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/svelte.png b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/svelte.png
new file mode 100644
index 00000000..e673c91c
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/svelte.png
Binary files differ
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp8.png b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp8.png
new file mode 100644
index 00000000..f9923be0
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp8.png
Binary files differ
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp9.png b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp9.png
new file mode 100644
index 00000000..1fcfffff
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/assets/vp9.png
Binary files differ
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Consumer.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Consumer.svelte
new file mode 100644
index 00000000..7dd1c0a3
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Consumer.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+ import type { ConsumerType } from '@/types/app'
+ import EncoderProps from '@/components/EncoderProps.svelte'
+
+ export let consumer: ConsumerType
+
+ $: id = consumer.id
+</script>
+
+<div class="consumer-card" on:click >
+ <div class="id">{id}</div>
+ <EncoderProps
+ consumer={consumer}
+ />
+</div>
+
+<style lang="scss">
+ .consumer-card {
+ word-break: break-all;
+ width: 150px;
+ height: 100px;
+ margin-right: 15px;
+ background-color: #fff;
+ padding: 15px;
+ border-radius: 10px;
+ box-shadow: rgb(0 0 0 / 24%) 0px 3px 8px;
+ .id {
+ font-weight: bold;
+ margin-bottom: 10px;
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/components/EncoderProps.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/EncoderProps.svelte
new file mode 100644
index 00000000..f6712d8e
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/EncoderProps.svelte
@@ -0,0 +1,53 @@
+<script lang="ts">
+ import type { ConsumerType } from '@/types/app'
+ import { MitigationMode } from '@/types/app'
+ import Fa from 'svelte-fa'
+ import { faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+ import vp8_logo from '@/assets/vp8.png'
+ import vp9_logo from '@/assets/vp9.png'
+ import h264_logo from '@/assets/h264.png'
+
+ export let consumer: ConsumerType
+
+ $: video_codec = consumer.video_codec
+ $: mitigation_mode = consumer.mitigation_mode
+</script>
+
+<div class="encoder-props">
+ <div class="codec">
+ {#if video_codec == "video/x-vp8"}
+ <img src={vp8_logo} alt="VP8">
+ {:else if video_codec == "video/x-vp9"}
+ <img src={vp9_logo} alt="VP8">
+ {:else if video_codec == "video/x-h264"}
+ <img src={h264_logo} alt="VP8">
+ {/if}
+ </div>
+ <div>
+ {#if mitigation_mode & MitigationMode.Downsampled && mitigation_mode & MitigationMode.Downscaled}
+ <abbr title="Very congested link, video is downscaled and downsampled">
+ <Fa icon={faExclamationTriangle} color="tomato" />
+ </abbr>
+ {:else if mitigation_mode & MitigationMode.Downscaled}
+ <abbr title="Congested link, video is downscaled">
+ <Fa icon={faExclamationTriangle} color="orange" />
+ </abbr>
+ {:else}
+ <abbr title="Link with minimal to no congestion">
+ <Fa icon={faCheckCircle} color="lightseagreen" />
+ </abbr>
+ {/if}
+ </div>
+</div>
+
+<style lang="scss">
+ .encoder-props {
+ display: flex;
+ justify-content: space-evenly;
+ .codec {
+ img {
+ width: 25px;
+ }
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Header.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Header.svelte
new file mode 100644
index 00000000..4dd4746a
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Header.svelte
@@ -0,0 +1,48 @@
+<script lang="ts">
+ import { WebSocketStatus } from '@/types/app'
+
+ import logo from '@/assets/svelte.png'
+ import Fa from 'svelte-fa'
+ import { faSpinner, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+
+ export let websocketStatus: WebSocketStatus
+</script>
+
+<header class="global-header">
+ <div class="app-name">
+ <img src={logo} alt="Svelte Logo" class="logo"/>
+ <div class="title">WebRTC Stats App</div>
+ <div>
+ {#if websocketStatus == WebSocketStatus.Connected}
+ <Fa icon={faCheckCircle} color="lightseagreen" size="1x" />
+ {:else if websocketStatus == WebSocketStatus.Connecting}
+ <Fa icon={faSpinner} color="#afaeae" size="1x" spin />
+ {:else if websocketStatus == WebSocketStatus.Error}
+ <Fa icon={faExclamationTriangle} color="tomato" size="1x" />
+ {/if}
+ </div>
+ </div>
+</header>
+
+<style lang="scss">
+ .global-header {
+ background-color: #313131;
+ height: 56px;
+ color: white;
+ padding: 15px;
+ box-sizing: border-box;
+ .app-name {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ .logo {
+ height: 100%;
+ margin-right: 5px;
+ }
+ .title {
+ font-weight: bold;
+ margin-right: 5px;
+ }
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Modal.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Modal.svelte
new file mode 100644
index 00000000..b4c7e881
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/Modal.svelte
@@ -0,0 +1,50 @@
+<script lang="ts">
+ import Fa from 'svelte-fa'
+ import { faTimes } from '@fortawesome/free-solid-svg-icons';
+ import { createEventDispatcher } from 'svelte';
+
+ const dispatch = createEventDispatcher();
+</script>
+
+<div class="modal-overlay">
+ <div class="modal">
+ <div class="modal-header">
+ <slot name="title"></slot>
+ <div class="close-icon" on:click="{() => dispatch('closeModal')}">
+ <Fa icon={faTimes} color="#afaeae" size="1x" />
+ </div>
+ </div>
+
+ <slot name="body"></slot>
+
+ <slot name="footer"></slot>
+ </div>
+</div>
+
+<style lang="scss">
+ .modal {
+ background-color: white;
+ padding: 15px 0;
+ border-radius: 10px;
+ box-shadow: rgb(0 0 0 / 24%) 0px 3px 8px;
+ &-overlay {
+ position: absolute;
+ top: 0;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgb(0, 0, 0, 0.4);
+ }
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 15px 10px;
+ border-bottom: 3px solid #e2e2e2;
+ }
+ .close-icon {
+ cursor: pointer;
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/components/PlotConsumerModal.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/PlotConsumerModal.svelte
new file mode 100644
index 00000000..c15ee13f
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/components/PlotConsumerModal.svelte
@@ -0,0 +1,142 @@
+<svelte:head>
+ <script src="https://cdn.plot.ly/plotly-latest.min.js" type="text/javascript"></script>
+</svelte:head>
+
+<script lang="ts">
+ import { createEventDispatcher, onMount, onDestroy } from 'svelte';
+ import Modal from '@/components/Modal.svelte'
+ import type { ConsumerType } from '@/types/app'
+ import EncoderProps from '@/components/EncoderProps.svelte'
+
+ export let consumer: ConsumerType
+
+ $: if (consumer === undefined) {
+ dispatch('close')
+ }
+
+ $: id = consumer !== undefined ? consumer.id : undefined
+
+ const dispatch = createEventDispatcher();
+ let interval: ReturnType<typeof setInterval> | undefined = undefined
+
+ onMount(() => {
+ let plotDiv = document.getElementById('plotDiv');
+
+ let traces = []
+ let layout = {
+ legend: {traceorder: 'reversed'},
+ height: 800,
+ }
+ let ctr = 1;
+ let domain_step = 1.0 / consumer.stats.size
+ let domain_margin = 0.05
+
+ for (let key of consumer.stats.keys()) {
+ let trace = {
+ x: [],
+ y: [],
+ xaxis: 'x' + ctr,
+ yaxis: 'y' + ctr,
+ mode: 'lines',
+ line: {shape: 'spline'},
+ name: key
+ }
+
+ traces.push(trace)
+
+ layout['xaxis' + ctr] = {
+ type: 'date',
+ }
+
+ layout['yaxis' + ctr] = {
+ domain: [(ctr - 1) * domain_step, (ctr * domain_step) - domain_margin],
+ rangemode: "tozero",
+ }
+
+ ctr += 1
+ }
+
+ Plotly.newPlot(plotDiv, traces, layout);
+
+ interval = setInterval(function() {
+ let time = new Date()
+ let ctr = 0
+ let traces = []
+ let data_update = {
+ x: [],
+ y: [],
+ }
+
+ for (let value of Object.values(consumer.stats)) {
+ data_update.x.push([time])
+ data_update.y.push([value])
+ traces.push(ctr)
+ ctr += 1
+ }
+
+ Plotly.extendTraces(plotDiv, data_update, traces, 600)
+
+ }, 50);
+ });
+
+ onDestroy(() => {
+ console.log ("destroyed")
+
+ if (interval !== undefined ) {
+ clearInterval (interval)
+ interval = undefined
+ }
+ })
+</script>
+
+<Modal on:closeModal="{() => dispatch('close')}">
+ <div slot="body" class="modal-body">
+ <div class="id">{id}</div>
+ <EncoderProps
+ consumer={consumer}
+ />
+ <div id="plotDiv"></div>
+ </div>
+
+ <div slot="footer" class="modal-footer">
+ <div class="buttons-wrapper">
+ <button
+ class="button"
+ on:click|stopPropagation="{() => dispatch('close')}"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+</Modal>
+
+<style lang="scss">
+ .modal {
+ &-body {
+ width: 1000px;
+ padding: 20px 15px 10px;
+ gap: 15px 0;
+ .id {
+ font-weight: bold;
+ margin-bottom: 10px;
+ }
+ }
+ &-footer {
+ padding: 0 15px;
+ .buttons-wrapper {
+ text-align: right;
+ }
+ .button {
+ height: 30px;
+ padding: 0 10px;
+ text-align: center;
+ box-sizing: content-box;
+ border-radius: 3px;
+ border: 1px solid #000;
+ &:active {
+ background-color: #b9b7b7;
+ }
+ }
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/main.ts b/net/webrtc/plugins/examples/webrtcsink-stats/src/main.ts
new file mode 100644
index 00000000..d8200ac4
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/main.ts
@@ -0,0 +1,7 @@
+import App from './App.svelte'
+
+const app = new App({
+ target: document.getElementById('app')
+})
+
+export default app
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/pages/Home.svelte b/net/webrtc/plugins/examples/webrtcsink-stats/src/pages/Home.svelte
new file mode 100644
index 00000000..44808535
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/pages/Home.svelte
@@ -0,0 +1,64 @@
+<script lang="ts">
+ import type { ConsumerType } from '@/types/app'
+ import Consumer from '@/components/Consumer.svelte'
+ import PlotConsumerModal from '@/components/PlotConsumerModal.svelte'
+
+ export let consumers: Array<ConsumerType>
+
+ let consumerToPlot: ConsumerType | undefined
+ let showPlotModal = false
+
+ /**
+ * Display the Plot modal
+ *
+ * @param {ConsumerType} consumer
+ */
+ const openPlotConsumer = (consumer: ConsumerType) => {
+ consumerToPlot = consumer
+ showPlotModal = true
+ }
+
+ /**
+ * Close the Plot modal
+ *
+ */
+ const closePlotConsumer = () => {
+ consumerToPlot = undefined
+ showPlotModal = false
+ }
+</script>
+
+<main>
+ <div class="consumer-card-container">
+ {#each consumers as consumer}
+ <Consumer
+ consumer = {consumer}
+ on:click="{() => { openPlotConsumer(consumer) }}"
+ />
+ {/each}
+ </div>
+</main>
+
+{#if showPlotModal}
+ <PlotConsumerModal
+ consumer={consumers.find(consumer => consumer == consumerToPlot)}
+ on:close="{closePlotConsumer}"
+ />
+{/if}
+
+<style lang="scss">
+ main {
+ padding: 2em;
+ margin: 0 auto;
+ width: 100vw;
+ box-sizing: border-box;
+ }
+ .consumer-card {
+ &-container {
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: 20px;
+ justify-content: space-evenly;
+ }
+ }
+</style>
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/types/app.ts b/net/webrtc/plugins/examples/webrtcsink-stats/src/types/app.ts
new file mode 100644
index 00000000..e34b8c5b
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/types/app.ts
@@ -0,0 +1,18 @@
+export enum MitigationMode {
+ None = 0,
+ Downscaled = 1,
+ Downsampled = 2,
+}
+
+export interface ConsumerType {
+ id: string,
+ video_codec: string | undefined,
+ mitigation_mode: MitigationMode,
+ stats: Map<string, number>,
+}
+
+export enum WebSocketStatus {
+ Connecting = 0,
+ Connected = 1,
+ Error = 2,
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/src/vite-env.d.ts b/net/webrtc/plugins/examples/webrtcsink-stats/src/vite-env.d.ts
new file mode 100644
index 00000000..4078e747
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="svelte" />
+/// <reference types="vite/client" />
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/svelte.config.js b/net/webrtc/plugins/examples/webrtcsink-stats/svelte.config.js
new file mode 100644
index 00000000..e8be9c97
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/svelte.config.js
@@ -0,0 +1,13 @@
+import sveltePreprocess from 'svelte-preprocess'
+import * as sass from 'sass'
+
+export default {
+ // Consult https://github.com/sveltejs/svelte-preprocess
+ // for more information about preprocessors
+ preprocess: sveltePreprocess({
+ sass: {
+ sync: true,
+ implementation: sass,
+ },
+ })
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/tsconfig.json b/net/webrtc/plugins/examples/webrtcsink-stats/tsconfig.json
new file mode 100644
index 00000000..b8e13491
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "baseUrl": ".",
+ /**
+ * Typechecking JS in `.svelte` and `.js` files by default.
+ * Disable checkJs if you'd like to use dynamic types in JS.
+ * Note that setting allowJs false does not prevent the use
+ * of JS in `.svelte` files.
+ */
+ "allowJs": true,
+ "checkJs": true,
+ "paths": {
+ "@/*": [
+ "src/*"
+ ],
+ }
+ },
+ "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/net/webrtc/plugins/examples/webrtcsink-stats/vite.config.js b/net/webrtc/plugins/examples/webrtcsink-stats/vite.config.js
new file mode 100644
index 00000000..67694a8a
--- /dev/null
+++ b/net/webrtc/plugins/examples/webrtcsink-stats/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+import path from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [svelte()],
+ resolve: {
+ alias: {
+ '@': path.resolve('/src'),
+ },
+ }
+})
diff --git a/net/webrtc/plugins/src/gcc/imp.rs b/net/webrtc/plugins/src/gcc/imp.rs
new file mode 100644
index 00000000..72fe8426
--- /dev/null
+++ b/net/webrtc/plugins/src/gcc/imp.rs
@@ -0,0 +1,1370 @@
+///! Implements https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02
+///!
+///! This element implements the pacing as describe in the spec by running its
+///! own streaming thread on its srcpad. It implements the mathematic as closely
+///! to the specs as possible and sets the #rtpgccbwe:estimated-bitrate property
+///! each time a new estimate is produced. User should connect to the
+///! `rtpgccbwe::notify::estimated-bitrate` signal to make the encoders target
+///! that new estimated bitrate (the overall target bitrate of the potentially
+///! multiple encore should match that target bitrate, the application is
+///! responsible for determining what bitrate to give to each encode)
+use chrono::Duration;
+use gst::{
+ glib::{self},
+ prelude::*,
+ subclass::prelude::*,
+};
+use once_cell::sync::Lazy;
+use std::{
+ collections::{BTreeMap, VecDeque},
+ fmt,
+ fmt::Debug,
+ mem,
+ sync::Mutex,
+ time,
+};
+
+type Bitrate = u32;
+
+const DEFAULT_MIN_BITRATE: Bitrate = 1000;
+const DEFAULT_ESTIMATED_BITRATE: Bitrate = 2_048_000;
+const DEFAULT_MAX_BITRATE: Bitrate = 8_192_000;
+
+static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "gcc",
+ gst::DebugColorFlags::empty(),
+ Some("Google Congestion Controller based bandwidth estimator"),
+ )
+});
+
+// Table1. Time limit in milliseconds between packet bursts which identifies a group
+static BURST_TIME: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(5));
+
+// Table1. Coefficient used for the measured noise variance
+// [0.1,0.001]
+const CHI: f64 = 0.01;
+const ONE_MINUS_CHI: f64 = 1. - CHI;
+
+// Table1. State noise covariance matrix
+const Q: f64 = 0.001;
+
+// Table1. Initial value for the adaptive threshold
+static INITIAL_DEL_VAR_TH: Lazy<Duration> = Lazy::new(|| Duration::microseconds(12500));
+
+// Table1. Initial value of the system error covariance
+const INITIAL_ERROR_COVARIANCE: f64 = 0.1;
+
+// Table1. Time required to trigger an overuse signal
+static OVERUSE_TIME_TH: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(10));
+
+// from 5.5 "beta is typically chosen to be in the interval [0.8, 0.95], 0.85 is the RECOMMENDED value."
+const BETA: f64 = 0.85;
+
+// From "5.5 Rate control" It is RECOMMENDED to measure this average and
+// standard deviation with an exponential moving average with the smoothing
+// factor 0.5 (NOTE: the spec mentions 0.95 here but in the equations it is 0.5
+// and other implementations use 0.5), as it is expected that this average
+// covers multiple occasions at which we are in the Decrease state.
+const MOVING_AVERAGE_SMOOTHING_FACTOR: f64 = 0.5;
+
+// `N(i)` is the number of packets received the past T seconds and `L(j)` is
+// the payload size of packet j. A window between 0.5 and 1 second is
+// RECOMMENDED.
+static PACKETS_RECEIVED_WINDOW: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(1000)); // ms
+
+// from "5.4 Over-use detector" ->
+// Moreover, del_var_th(i) SHOULD NOT be updated if this condition
+// holds:
+//
+// ```
+// |m(i)| - del_var_th(i) > 15
+// ```
+static MAX_M_MINUS_DEL_VAR_TH: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(15));
+
+// from 5.4 "It is also RECOMMENDED to clamp del_var_th(i) to the range [6, 600]"
+static MIN_THRESHOLD: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(6));
+static MAX_THRESHOLD: Lazy<Duration> = Lazy::new(|| Duration::milliseconds(600));
+
+// From 5.5 ""Close" is defined as three standard deviations around this average"
+const STANDARD_DEVIATION_CLOSE_NUM: f64 = 3.;
+
+// Minimal duration between 2 updates on the lost based rate controller
+static LOSS_UPDATE_INTERVAL: Lazy<time::Duration> = Lazy::new(|| time::Duration::from_millis(200));
+static LOSS_DECREASE_THRESHOLD: f64 = 0.1;
+static LOSS_INCREASE_THRESHOLD: f64 = 0.02;
+static LOSS_INCREASE_FACTOR: f64 = 1.05;
+
+// Minimal duration between 2 updates on the lost based rate controller
+static DELAY_UPDATE_INTERVAL: Lazy<time::Duration> = Lazy::new(|| time::Duration::from_millis(100));
+
+static ROUND_TRIP_TIME_WINDOW_SIZE: usize = 100;
+
+fn ts2dur(t: gst::ClockTime) -> Duration {
+ Duration::nanoseconds(t.nseconds() as i64)
+}
+
+fn dur2ts(t: Duration) -> gst::ClockTime {
+ gst::ClockTime::from_nseconds(t.num_nanoseconds().unwrap() as u64)
+}
+
+#[derive(Debug)]
+enum BandwidthEstimationOp {
+ /// Don't update target bitrate
+ Hold,
+ /// Decrease target bitrate
+ Decrease(String /* reason */),
+ Increase(String /* reason */),
+}
+
+#[derive(Debug, Clone, Copy)]
+enum ControllerType {
+ // Running the "delay-based controller"
+ Delay,
+ // Running the "loss based controller"
+ Loss,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Packet {
+ departure: Duration,
+ arrival: Duration,
+ size: usize,
+ seqnum: u64,
+}
+
+fn human_kbits<T: Into<f64>>(bits: T) -> String {
+ format!("{:.2}kb", (bits.into() / 1_000.))
+}
+
+impl Packet {
+ fn from_structure(structure: &gst::StructureRef) -> Option<Self> {
+ let lost = structure.get::<bool>("lost").unwrap();
+ let departure = match structure.get::<gst::ClockTime>("local-ts") {
+ Err(e) => {
+ gst::fixme!(
+ CAT,
+ "Got packet feedback without local-ts: {:?} - what does that mean?",
+ e
+ );
+ return None;
+ }
+ Ok(ts) => ts,
+ };
+
+ let seqnum = structure.get::<u32>("seqnum").unwrap() as u64;
+ if lost {
+ return Some(Packet {
+ arrival: Duration::zero(),
+ departure: ts2dur(departure),
+ size: structure.get::<u32>("size").unwrap() as usize,
+ seqnum,
+ });
+ }
+
+ let arrival = structure.get::<gst::ClockTime>("remote-ts").unwrap();
+
+ Some(Packet {
+ arrival: ts2dur(arrival),
+ departure: ts2dur(departure),
+ size: structure.get::<u32>("size").unwrap() as usize,
+ seqnum,
+ })
+ }
+}
+
+#[derive(Clone)]
+struct PacketGroup {
+ packets: Vec<Packet>,
+ departure: Duration, // ms
+ arrival: Option<Duration>, // ms
+}
+
+impl Default for PacketGroup {
+ fn default() -> Self {
+ Self {
+ packets: Default::default(),
+ departure: Duration::zero(),
+ arrival: None,
+ }
+ }
+}
+
+fn pdur(d: &Duration) -> String {
+ let stdd = time::Duration::from_nanos(d.num_nanoseconds().unwrap().abs() as u64);
+
+ format!("{}{stdd:?}", if d.lt(&Duration::zero()) { "-" } else { "" })
+}
+
+impl PacketGroup {
+ fn add(&mut self, packet: Packet) {
+ if self.departure.is_zero() {
+ self.departure = packet.departure;
+ }
+
+ self.arrival = Some(
+ self.arrival
+ .map_or_else(|| packet.arrival, |v| Duration::max(v, packet.arrival)),
+ );
+ self.packets.push(packet);
+ }
+
+ /// Returns the delta between self.arrival_time and @prev_group.arrival_time in ms
+ // t(i) - t(i-1)
+ fn inter_arrival_time(&self, prev_group: &Self) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ self.arrival.unwrap() - prev_group.arrival.unwrap()
+ }
+
+ fn inter_arrival_time_pkt(&self, next_pkt: &Packet) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ next_pkt.arrival - self.arrival.unwrap()
+ }
+
+ /// Returns the delta between self.departure_time and @prev_group.departure_time in ms
+ // T(i) - T(i-1)
+ fn inter_departure_time(&self, prev_group: &Self) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ self.departure - prev_group.departure
+ }
+
+ fn inter_departure_time_pkt(&self, next_pkt: &Packet) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ next_pkt.departure - self.departure
+ }
+
+ /// Returns the delta between intern arrival time and inter departure time in ms
+ fn inter_delay_variation(&self, prev_group: &Self) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ self.inter_arrival_time(prev_group) - self.inter_departure_time(prev_group)
+ }
+
+ fn inter_delay_variation_pkt(&self, next_pkt: &Packet) -> Duration {
+ // Should never be called if we haven't gotten feedback for all
+ // contained packets
+ self.inter_arrival_time_pkt(next_pkt) - self.inter_departure_time_pkt(next_pkt)
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+enum NetworkUsage {
+ Normal,
+ Over,
+ Under,
+}
+
+struct Detector {
+ group: PacketGroup, // Packet group that is being filled
+ prev_group: Option<PacketGroup>, // Group that is ready to be used once "group" is filled
+ measure: Duration, // Delay variation measure
+
+ last_received_packets: BTreeMap<u64, Packet>, // Order by seqnums, front is the newest, back is the oldest
+
+ // Last loss update
+ last_loss_update: Option<time::Instant>,
+ // Moving average of the packet loss
+ loss_average: f64,
+
+ // Kalman filter fields
+ gain: f64,
+ measurement_uncertainty: f64, // var_v_hat(i-1)
+ estimate_error: f64, // e(i-1)
+ estimate: Duration, // m_hat(i-1)
+
+ // Threshold fields
+ threshold: Duration,
+ last_threshold_update: Option<time::Instant>,
+ num_deltas: i64,
+
+ // Overuse related fields
+ increasing_counter: u32,
+ last_overuse_estimate: Duration,
+ last_use_detector_update: time::Instant,
+ increasing_duration: Duration,
+
+ // round-trip-time estimations
+ rtts: VecDeque<Duration>,
+ clock: gst::Clock,
+
+ // Current network usage state
+ usage: NetworkUsage,
+
+ twcc_extended_seqnum: u64,
+}
+
+// Monitors packet loss and network overuse through because of delay
+impl Debug for Detector {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "Network Usage: {:?}. Effective bitrate: {}ps - Measure: {} Estimate: {} threshold {} - overuse_estimate {}",
+ self.usage,
+ human_kbits(self.effective_bitrate()),
+ pdur(&self.measure),
+ pdur(&self.estimate),
+ pdur(&self.threshold),
+ pdur(&self.last_overuse_estimate),
+ )
+ }
+}
+
+impl Detector {
+ fn new() -> Self {
+ Detector {
+ group: Default::default(),
+ prev_group: Default::default(),
+ measure: Duration::zero(),
+
+ /* Smallish value to hold PACKETS_RECEIVED_WINDOW packets */
+ last_received_packets: BTreeMap::new(),
+
+ last_loss_update: None,
+ loss_average: 0.,
+
+ gain: 0.,
+ measurement_uncertainty: 0.,
+ estimate_error: INITIAL_ERROR_COVARIANCE,
+ estimate: Duration::zero(),
+
+ threshold: *INITIAL_DEL_VAR_TH,
+ last_threshold_update: None,
+ num_deltas: 0,
+
+ last_use_detector_update: time::Instant::now(),
+ increasing_counter: 0,
+ last_overuse_estimate: Duration::zero(),
+ increasing_duration: Duration::zero(),
+
+ rtts: Default::default(),
+ clock: gst::SystemClock::obtain(),
+
+ usage: NetworkUsage::Normal,
+
+ twcc_extended_seqnum: 0,
+ }
+ }
+
+ fn loss_ratio(&self) -> f64 {
+ self.loss_average
+ }
+
+ fn update_last_received_packets(&mut self, packet: Packet) {
+ self.last_received_packets.insert(packet.seqnum, packet);
+ self.evict_old_received_packets();
+ }
+
+ fn evict_old_received_packets(&mut self) {
+ let last_arrival = self
+ .last_received_packets
+ .values()
+ .next_back()
+ .unwrap()
+ .arrival;
+
+ while last_arrival - self.oldest_packet_in_window_ts() > *PACKETS_RECEIVED_WINDOW {
+ let oldest_seqnum = self.last_received_packets.iter().next().unwrap().0.clone();
+ self.last_received_packets.remove(&oldest_seqnum);
+ }
+ }
+
+ /// Returns the effective received bitrate during the last PACKETS_RECEIVED_WINDOW
+ fn effective_bitrate(&self) -> Bitrate {
+ if self.last_received_packets.is_empty() {
+ return 0;
+ }
+
+ let duration = self
+ .last_received_packets
+ .iter()
+ .next_back()
+ .unwrap()
+ .1
+ .arrival
+ - self.last_received_packets.iter().next().unwrap().1.arrival;
+ let bits = self
+ .last_received_packets
+ .iter()
+ .map(|(_seqnum, p)| p.size as f64)
+ .sum::<f64>()
+ * 8.;
+
+ (bits
+ / (duration.num_nanoseconds().unwrap() as f64
+ / gst::ClockTime::SECOND.nseconds() as f64)) as Bitrate
+ }
+
+ fn oldest_packet_in_window_ts(&self) -> Duration {
+ self.last_received_packets.iter().next().unwrap().1.arrival
+ }
+
+ fn update_rtts(&mut self, packets: &Vec<Packet>) {
+ let mut rtt = Duration::nanoseconds(i64::MAX);
+ let now = ts2dur(self.clock.time().unwrap());
+ for packet in packets {
+ rtt = (now - packet.departure).min(rtt);
+ }
+
+ self.rtts.push_back(rtt);
+ if self.rtts.len() > ROUND_TRIP_TIME_WINDOW_SIZE {
+ self.rtts.pop_front();
+ }
+ }
+
+ fn rtt(&self) -> Duration {
+ Duration::nanoseconds(
+ (self
+ .rtts
+ .iter()
+ .map(|d| d.num_nanoseconds().unwrap() as f64)
+ .sum::<f64>()
+ / self.rtts.len() as f64) as i64,
+ )
+ }
+
+ fn update(&mut self, packets: &mut Vec<Packet>) {
+ self.update_rtts(packets);
+ let mut lost_packets = 0.;
+ let n_packets = packets.len();
+ for pkt in packets {
+ // We know feedbacks packets will arrive "soon" after the packets they are reported for or considered
+ // lost so we can make the assumption that
+ let mut seqnum = pkt.seqnum + (self.twcc_extended_seqnum & !(0xffff as u64));
+
+ if seqnum < self.twcc_extended_seqnum {
+ let diff = self.twcc_extended_seqnum.overflowing_sub(seqnum).0;
+
+ if diff > i16::MAX as u64 {
+ seqnum += 1 << 16;
+ }
+ } else {
+ let diff = seqnum.overflowing_sub(self.twcc_extended_seqnum).0;
+
+ if diff > i16::MAX as u64 {
+ if seqnum < 1 << 16 {
+ eprintln!("Cannot unwrap, any wrapping took place yet. Returning 0 without updating extended timestamp.");
+ } else {
+ seqnum -= 1 << 16;
+ }
+ }
+ }
+
+ self.twcc_extended_seqnum = u64::max(seqnum, self.twcc_extended_seqnum);
+
+ pkt.seqnum = seqnum;
+
+ if pkt.arrival.is_zero() {
+ lost_packets += 1.;
+ continue;
+ }
+
+ self.update_last_received_packets(*pkt);
+
+ if self.group.arrival.is_none() {
+ self.group.add(*pkt);
+
+ continue;
+ }
+
+ if pkt.arrival < self.group.arrival.unwrap() {
+ // ignore out of order arrivals
+ continue;
+ }
+
+ if pkt.departure >= self.group.departure {
+ if self.group.inter_departure_time_pkt(pkt) < *BURST_TIME {
+ self.group.add(*pkt);
+ continue;
+ }
+
+ // 5.2 Pre-filtering
+ //
+ // A Packet which has an inter-arrival time less than burst_time and
+ // an inter-group delay variation d(i) less than 0 is considered
+ // being part of the current group of packets.
+ if self.group.inter_arrival_time_pkt(pkt) < *BURST_TIME
+ && self.group.inter_delay_variation_pkt(pkt) < Duration::zero()
+ {
+ self.group.add(*pkt);
+ continue;
+ }
+
+ let group = mem::take(&mut self.group);
+ gst::trace!(
+ CAT,
+ "Packet group done: {:?}",
+ gst::ClockTime::from_nseconds(group.departure.num_nanoseconds().unwrap() as u64)
+ );
+ if let Some(prev_group) = mem::replace(&mut self.prev_group, Some(group.clone())) {
+ // 5.3 Arrival-time filter
+ self.kalman_estimate(&prev_group, &group);
+ // 5.4 Over-use detector
+ self.overuse_filter();
+ }
+ } else {
+ gst::debug!(
+ CAT,
+ "Ignoring packet departed at {:?} as we got feedback too late",
+ gst::ClockTime::from_nseconds(pkt.departure.num_nanoseconds().unwrap() as u64)
+ );
+ }
+ }
+
+ self.compute_loss_average(lost_packets as f64 / n_packets as f64);
+ }
+
+ fn compute_loss_average(&mut self, loss_fraction: f64) {
+ let now = time::Instant::now();
+
+ if let Some(ref last_update) = self.last_loss_update {
+ self.loss_average = loss_fraction
+ + (-Duration::from_std(now - *last_update)
+ .unwrap()
+ .num_milliseconds() as f64)
+ .exp()
+ * (self.loss_average - loss_fraction);
+ }
+
+ self.last_loss_update = Some(now);
+ }
+
+ fn kalman_estimate(&mut self, prev_group: &PacketGroup, group: &PacketGroup) {
+ self.measure = group.inter_delay_variation(prev_group);
+
+ let z = self.measure - self.estimate;
+ let zms = z.num_microseconds().unwrap() as f64 / 1000.0;
+
+ // This doesn't exactly follows the spec as we should compute and
+ // use f_max here, no implementation we have found actually uses it.
+ let alpha = ONE_MINUS_CHI.powf(30.0 / (1000. * 5. * 1_000_000.));
+ let root = self.measurement_uncertainty.sqrt();
+ let root3 = 3. * root;
+
+ if zms > root3 {
+ self.measurement_uncertainty =
+ (alpha * self.measurement_uncertainty + (1. - alpha) * root3.powf(2.)).max(1.);
+ } else {
+ self.measurement_uncertainty =
+ (alpha * self.measurement_uncertainty + (1. - alpha) * zms.powf(2.)).max(1.);
+ }
+
+ let estimate_uncertainty = self.estimate_error + Q;
+ self.gain = estimate_uncertainty / (estimate_uncertainty + self.measurement_uncertainty);
+ self.estimate =
+ self.estimate + Duration::nanoseconds((self.gain * zms * 1_000_000.) as i64);
+ self.estimate_error = (1. - self.gain) * estimate_uncertainty;
+ }
+
+ fn compare_threshold(&mut self) -> (NetworkUsage, Duration) {
+ // FIXME: It is unclear where that factor is coming from but all
+ // implementations we found have it (libwebrtc, pion, jitsi...), and the
+ // algorithm does not work without it.
+ const MAX_DELTAS: i64 = 60;
+
+ self.num_deltas += 1;
+ if self.num_deltas < 2 {
+ return (NetworkUsage::Normal, self.estimate);
+ }
+
+ let t = Duration::nanoseconds(
+ self.estimate.num_nanoseconds().unwrap() * i64::min(self.num_deltas, MAX_DELTAS),
+ );
+ let usage = if t > self.threshold {
+ NetworkUsage::Over
+ } else if t.num_nanoseconds().unwrap() < -self.threshold.num_nanoseconds().unwrap() {
+ NetworkUsage::Under
+ } else {
+ NetworkUsage::Normal
+ };
+
+ self.update_threshold(&t);
+
+ (usage, t)
+ }
+
+ fn update_threshold(&mut self, estimate: &Duration) {
+ const K_U: f64 = 0.01; // Table1. Coefficient for the adaptive threshold
+ const K_D: f64 = 0.00018; // Table1. Coefficient for the adaptive threshold
+ const MAX_TIME_DELTA: time::Duration = time::Duration::from_millis(100);
+
+ let now = time::Instant::now();
+ if self.last_threshold_update.is_none() {
+ self.last_threshold_update = Some(now);
+ }
+
+ let abs_estimate = Duration::nanoseconds(estimate.num_nanoseconds().unwrap().abs());
+ if abs_estimate > self.threshold + *MAX_M_MINUS_DEL_VAR_TH {
+ self.last_threshold_update = Some(now);
+ return;
+ }
+
+ let k = if abs_estimate < self.threshold {
+ K_D
+ } else {
+ K_U
+ };
+ let time_delta =
+ Duration::from_std((now - self.last_threshold_update.unwrap()).min(MAX_TIME_DELTA))
+ .unwrap();
+ let d = abs_estimate - self.threshold;
+ let add = k * d.num_milliseconds() as f64 * time_delta.num_milliseconds() as f64;
+
+ self.threshold = self.threshold + Duration::nanoseconds((add * 100. * 1_000.) as i64);
+ self.threshold = self.threshold.clamp(*MIN_THRESHOLD, *MAX_THRESHOLD);
+ self.last_threshold_update = Some(now);
+ }
+
+ fn overuse_filter(&mut self) {
+ let (th_usage, estimate) = self.compare_threshold();
+
+ let now = time::Instant::now();
+ let delta = Duration::from_std(now - self.last_use_detector_update).unwrap();
+ self.last_use_detector_update = now;
+ gst::log!(
+ CAT,
+ "{:?} - self.estimate {} - estimate: {} - th: {}",
+ th_usage,
+ pdur(&self.estimate),
+ pdur(&estimate),
+ pdur(&self.threshold)
+ );
+ match th_usage {
+ NetworkUsage::Over => {
+ self.increasing_duration = self.increasing_duration + delta;
+ self.increasing_counter += 1;
+
+ if self.increasing_duration > *OVERUSE_TIME_TH
+ && self.increasing_counter > 1
+ && estimate > self.last_overuse_estimate
+ {
+ self.usage = NetworkUsage::Over;
+ }
+ }
+ NetworkUsage::Under | NetworkUsage::Normal => {
+ self.increasing_duration = Duration::zero();
+ self.increasing_counter = 0;
+
+ self.usage = th_usage;
+ }
+ }
+ self.last_overuse_estimate = estimate;
+ }
+}
+
+#[derive(Default, Debug)]
+struct ExponentialMovingAverage {
+ average: Option<f64>,
+ variance: f64,
+ standard_dev: f64,
+}
+
+impl ExponentialMovingAverage {
+ fn update<T: Into<f64>>(&mut self, value: T) {
+ if let Some(avg) = self.average {
+ let avg_diff = value.into() - avg;
+
+ self.variance = (1. - MOVING_AVERAGE_SMOOTHING_FACTOR)
+ * (self.variance + MOVING_AVERAGE_SMOOTHING_FACTOR * avg_diff * avg_diff);
+ self.standard_dev = self.variance.sqrt();
+
+ self.average = Some(avg + (MOVING_AVERAGE_SMOOTHING_FACTOR * avg_diff));
+ } else {
+ self.average = Some(value.into());
+ }
+ }
+
+ fn estimate_is_close(&self, value: Bitrate) -> bool {
+ self.average.map_or(false, |avg| {
+ ((avg - STANDARD_DEVIATION_CLOSE_NUM * self.standard_dev)
+ ..(avg + STANDARD_DEVIATION_CLOSE_NUM * self.standard_dev))
+ .contains(&(value as f64))
+ })
+ }
+}
+
+struct State {
+ /// Note: The target bitrate applied is the min of
+ /// target_bitrate_on_delay and target_bitrate_on_loss
+ estimated_bitrate: Bitrate,
+
+ /// Bitrate target based on delay factor for all video streams.
+ /// Hasn't been tested with multiple video streams, but
+ /// current design is simply to divide bitrate equally.
+ target_bitrate_on_delay: Bitrate,
+
+ /// Used in additive mode to track last control time, influences
+ /// calculation of added value according to gcc section 5.5
+ last_increase_on_delay: Option<time::Instant>,
+ last_decrease_on_delay: time::Instant,
+
+ /// Bitrate target based on loss for all video streams.
+ target_bitrate_on_loss: Bitrate,
+
+ last_increase_on_loss: time::Instant,
+ last_decrease_on_loss: time::Instant,
+
+ /// Exponential moving average, updated when bitrate is
+ /// decreased
+ ema: ExponentialMovingAverage,
+
+ last_control_op: BandwidthEstimationOp,
+
+ min_bitrate: Bitrate,
+ max_bitrate: Bitrate,
+
+ detector: Detector,
+
+ clock_entry: Option<gst::SingleShotClockId>,
+
+ // Implemented like a leaky bucket
+ buffers: VecDeque<gst::Buffer>,
+ // Number of bits remaining from previous burst
+ budget_offset: i64,
+
+ flow_return: Result<gst::FlowSuccess, gst::FlowError>,
+ last_push: time::Instant,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ target_bitrate_on_delay: DEFAULT_ESTIMATED_BITRATE,
+ target_bitrate_on_loss: DEFAULT_ESTIMATED_BITRATE,
+ last_increase_on_loss: time::Instant::now(),
+ last_decrease_on_loss: time::Instant::now(),
+ ema: Default::default(),
+ last_increase_on_delay: None,
+ last_decrease_on_delay: time::Instant::now(),
+ min_bitrate: DEFAULT_MIN_BITRATE,
+ max_bitrate: DEFAULT_MAX_BITRATE,
+ detector: Detector::new(),
+ buffers: Default::default(),
+ estimated_bitrate: DEFAULT_ESTIMATED_BITRATE,
+ last_control_op: BandwidthEstimationOp::Increase("Initial increase".into()),
+ flow_return: Err(gst::FlowError::Flushing),
+ clock_entry: None,
+ last_push: time::Instant::now(),
+ budget_offset: 0,
+ }
+ }
+}
+
+impl State {
+ // 4. sending engine implementing a "leaky bucket"
+ fn create_buffer_list(&mut self, bwe: &super::BandwidthEstimator) -> gst::BufferList {
+ let now = time::Instant::now();
+ let elapsed = Duration::from_std(now - self.last_push).unwrap();
+ let mut budget = (elapsed.num_nanoseconds().unwrap())
+ .mul_div_round(
+ self.estimated_bitrate as i64,
+ gst::ClockTime::SECOND.nseconds() as i64,
+ )
+ .unwrap()
+ + self.budget_offset;
+ let total_budget = budget;
+ let mut remaining = self.buffers.iter().map(|b| b.size() as f64).sum::<f64>() * 8.;
+ let total_size = remaining;
+
+ let mut list = gst::BufferList::new();
+ let mutlist = list.get_mut().unwrap();
+
+ // Leak the bucket so it can hold at most 30ms of data
+ let maximum_remaining_bits = 30. * self.estimated_bitrate as f64 / 1000.;
+ let mut leaked = false;
+ while (budget > 0 || remaining > maximum_remaining_bits) && !self.buffers.is_empty() {
+ let buf = self.buffers.pop_back().unwrap();
+ let n_bits = buf.size() * 8;
+
+ leaked = budget <= 0 && remaining > maximum_remaining_bits;
+ mutlist.add(buf);
+ budget -= n_bits as i64;
+ remaining -= n_bits as f64;
+ }
+
+ gst::trace!(
+ CAT,
+ obj: bwe,
+ "{} bitrate: {}ps budget: {}/{} sending: {} Remaining: {}/{}",
+ pdur(&elapsed),
+ human_kbits(self.estimated_bitrate),
+ human_kbits(budget as f64),
+ human_kbits(total_budget as f64),
+ human_kbits(list.calculate_size() as f64 * 8.),
+ human_kbits(remaining),
+ human_kbits(total_size)
+ );
+
+ self.last_push = now;
+ self.budget_offset = if !leaked { budget } else { 0 };
+
+ list
+ }
+
+ fn compute_increased_rate(&mut self, bwe: &super::BandwidthEstimator) -> Option<Bitrate> {
+ let now = time::Instant::now();
+ let target_bitrate = self.target_bitrate_on_delay as f64;
+ let effective_bitrate = self.detector.effective_bitrate();
+ let time_since_last_update_ms = match self.last_increase_on_delay {
+ None => 0.,
+ Some(prev) => {
+ if now - prev < *DELAY_UPDATE_INTERVAL {
+ return None;
+ }
+
+ (now - prev).as_millis() as f64
+ }
+ };
+
+ if effective_bitrate as f64 - target_bitrate as f64 > 5. * target_bitrate / 100. {
+ gst::info!(
+ CAT,
+ "Effective rate {} >> target bitrate {} - we should avoid that \
+ as much as possible fine tuning the encoder",
+ human_kbits(effective_bitrate),
+ human_kbits(target_bitrate)
+ );
+ }
+
+ self.last_increase_on_delay = Some(now);
+ if self.ema.estimate_is_close(effective_bitrate) {
+ let bits_per_frame = target_bitrate / 30.;
+ let packets_per_frame = f64::ceil(bits_per_frame / (1200. * 8.));
+ let avg_packet_size_bits = bits_per_frame / packets_per_frame;
+
+ let rtt_ms = self.detector.rtt().num_milliseconds() as f64;
+ let response_time_ms = 100. + rtt_ms;
+ let alpha = 0.5 * f64::min(time_since_last_update_ms / response_time_ms, 1.0);
+ let threshold_on_effective_bitrate = 1.5 * effective_bitrate as f64;
+ let increase = f64::max(
+ 1000.0f64,
+ f64::min(
+ alpha * avg_packet_size_bits,
+ // Stuffing should ensure that the effective bitrate is not
+ // < target bitrate, still, make sure to always increase
+ // the bitrate by a minimum amount of 160.bits
+ f64::max(
+ threshold_on_effective_bitrate - self.target_bitrate_on_delay as f64,
+ 160.,
+ ),
+ ),
+ );
+
+ /* Additive increase */
+ self.last_control_op =
+ BandwidthEstimationOp::Increase(format!("Additive ({})", human_kbits(increase)));
+ Some((self.target_bitrate_on_delay as f64 + increase) as Bitrate)
+ } else {
+ let eta = 1.08_f64.powf(f64::min(time_since_last_update_ms / 1000., 1.0));
+ let rate = eta * self.target_bitrate_on_delay as f64;
+
+ self.ema = Default::default();
+
+ assert!(
+ rate >= self.target_bitrate_on_delay as f64,
+ "Increase: {} - {}",
+ rate,
+ eta
+ );
+
+ // Maximum increase to 1.5 * received rate
+ let received_max = 1.5 * effective_bitrate as f64;
+
+ if rate > received_max && received_max > self.target_bitrate_on_delay as f64 {
+ gst::log!(
+ CAT,
+ obj: bwe,
+ "Increasing == received_max rate: {}ps",
+ human_kbits(received_max)
+ );
+
+ self.last_control_op = BandwidthEstimationOp::Increase(format!(
+ "Using 1.5*effective_rate({})",
+ human_kbits(effective_bitrate)
+ ));
+ Some(received_max as Bitrate)
+ } else if rate < self.target_bitrate_on_delay as f64 {
+ gst::log!(
+ CAT,
+ obj: bwe,
+ "Rate < target, returning {}ps",
+ human_kbits(self.target_bitrate_on_delay)
+ );
+
+ None
+ } else {
+ gst::log!(
+ CAT,
+ obj: bwe,
+ "Increase mult {eta}x{}ps={}ps",
+ human_kbits(self.target_bitrate_on_delay),
+ human_kbits(rate)
+ );
+
+ self.last_control_op =
+ BandwidthEstimationOp::Increase(format!("Multiplicative x{eta}"));
+ Some(rate as Bitrate)
+ }
+ }
+ }
+
+ fn set_bitrate(
+ &mut self,
+ bwe: &super::BandwidthEstimator,
+ bitrate: Bitrate,
+ controller_type: ControllerType,
+ ) -> bool {
+ let prev_bitrate = Bitrate::min(self.target_bitrate_on_delay, self.target_bitrate_on_loss);
+
+ match controller_type {
+ ControllerType::Loss => {
+ self.target_bitrate_on_loss = bitrate.clamp(self.min_bitrate, self.max_bitrate)
+ }
+
+ ControllerType::Delay => {
+ self.target_bitrate_on_delay = bitrate.clamp(self.min_bitrate, self.max_bitrate)
+ }
+ }
+
+ let target_bitrate =
+ Bitrate::min(self.target_bitrate_on_delay, self.target_bitrate_on_loss)
+ .clamp(self.min_bitrate, self.max_bitrate);
+
+ if target_bitrate == prev_bitrate {
+ return false;
+ }
+
+ gst::info!(
+ CAT,
+ obj: bwe,
+ "{controller_type:?}: {}ps => {}ps ({:?}) - effective bitrate: {}",
+ human_kbits(prev_bitrate),
+ human_kbits(target_bitrate),
+ self.last_control_op,
+ human_kbits(self.detector.effective_bitrate()),
+ );
+
+ self.estimated_bitrate = target_bitrate;
+
+ true
+ }
+
+ fn loss_control(&mut self, bwe: &super::BandwidthEstimator) -> bool {
+ let loss_ratio = self.detector.loss_ratio();
+ let now = time::Instant::now();
+
+ if loss_ratio > LOSS_DECREASE_THRESHOLD
+ && (now - self.last_decrease_on_loss) > *LOSS_UPDATE_INTERVAL
+ {
+ let factor = 1. - (0.5 * loss_ratio);
+
+ self.last_control_op =
+ BandwidthEstimationOp::Decrease(format!("High loss detected ({loss_ratio:2}"));
+ self.last_decrease_on_loss = now;
+
+ self.set_bitrate(
+ bwe,
+ (self.target_bitrate_on_loss as f64 * factor) as Bitrate,
+ ControllerType::Loss,
+ )
+ } else if loss_ratio < LOSS_INCREASE_THRESHOLD
+ && (now - self.last_increase_on_loss) > *LOSS_UPDATE_INTERVAL
+ {
+ self.last_control_op = BandwidthEstimationOp::Increase("Low loss".into());
+ self.last_increase_on_loss = now;
+
+ self.set_bitrate(
+ bwe,
+ (self.target_bitrate_on_loss as f64 * LOSS_INCREASE_FACTOR) as Bitrate,
+ ControllerType::Loss,
+ )
+ } else {
+ false
+ }
+ }
+
+ fn delay_control(&mut self, bwe: &super::BandwidthEstimator) -> bool {
+ match self.detector.usage {
+ NetworkUsage::Normal => match self.last_control_op {
+ BandwidthEstimationOp::Increase(..) | BandwidthEstimationOp::Hold => {
+ if let Some(bitrate) = self.compute_increased_rate(bwe) {
+ return self.set_bitrate(bwe, bitrate, ControllerType::Delay);
+ }
+ }
+ _ => (),
+ },
+ NetworkUsage::Over => {
+ let now = time::Instant::now();
+ if now - self.last_decrease_on_delay > *DELAY_UPDATE_INTERVAL {
+ let effective_bitrate = self.detector.effective_bitrate();
+ let target =
+ (self.estimated_bitrate as f64 * 0.95).min(BETA * effective_bitrate as f64);
+ self.last_control_op = BandwidthEstimationOp::Decrease(format!(
+ "Over use detected {:#?}",
+ self.detector
+ ));
+ self.ema.update(effective_bitrate);
+ self.last_decrease_on_delay = now;
+
+ return self.set_bitrate(bwe, target as Bitrate, ControllerType::Delay);
+ }
+ }
+ NetworkUsage::Under => {
+ if let BandwidthEstimationOp::Increase(..) = self.last_control_op {
+ if let Some(bitrate) = self.compute_increased_rate(bwe) {
+ return self.set_bitrate(bwe, bitrate, ControllerType::Delay);
+ }
+ }
+ }
+ }
+
+ self.last_control_op = BandwidthEstimationOp::Hold;
+
+ false
+ }
+}
+
+pub struct BandwidthEstimator {
+ state: Mutex<State>,
+
+ srcpad: gst::Pad,
+ sinkpad: gst::Pad,
+}
+
+impl BandwidthEstimator {
+ fn push_list(&self, list: gst::BufferList) -> Result<gst::FlowSuccess, gst::FlowError> {
+ let res = self.srcpad.push_list(list);
+
+ self.state.lock().unwrap().flow_return = res;
+
+ res
+ }
+
+ fn start_task(&self, bwe: &super::BandwidthEstimator) -> Result<(), glib::BoolError> {
+ let weak_bwe = bwe.downgrade();
+ let weak_pad = self.srcpad.downgrade();
+ let clock = gst::SystemClock::obtain();
+
+ bwe.imp().state.lock().unwrap().clock_entry =
+ Some(clock.new_single_shot_id(clock.time().unwrap() + dur2ts(*BURST_TIME)));
+
+ self.srcpad.start_task(move || {
+ let pause = || {
+ if let Some(pad) = weak_pad.upgrade() {
+ let _ = pad.pause_task();
+ }
+ };
+ let bwe = weak_bwe
+ .upgrade()
+ .expect("bwe destroyed while its srcpad task is still running?");
+
+ let lock_state = || bwe.imp().state.lock().unwrap();
+
+ let clock_entry = match lock_state().clock_entry.take() {
+ Some(id) => id,
+ _ => {
+ gst::info!(CAT, "Pausing task as our clock entry is not set anymore");
+ return pause();
+ }
+ };
+
+ if let (Err(err), _) = clock_entry.wait() {
+ match err {
+ gst::ClockError::Early => (),
+ _ => {
+ gst::error!(CAT, "Got error {err:?} on the clock, pausing task");
+
+ lock_state().flow_return = Err(gst::FlowError::Flushing);
+
+ return pause();
+ }
+ }
+ }
+ let list = {
+ let mut state = lock_state();
+ clock
+ .single_shot_id_reinit(
+ &clock_entry,
+ clock.time().unwrap() + dur2ts(*BURST_TIME),
+ )
+ .unwrap();
+ state.clock_entry = Some(clock_entry);
+ state.create_buffer_list(&bwe)
+ };
+
+ if !list.is_empty() {
+ if let Err(err) = bwe.imp().push_list(list) {
+ gst::error!(CAT, obj: &bwe, "pause task, reason: {err:?}");
+ return pause();
+ }
+ }
+ })?;
+
+ Ok(())
+ }
+
+ fn src_activatemode(
+ &self,
+ _pad: &gst::Pad,
+ bwe: &super::BandwidthEstimator,
+ mode: gst::PadMode,
+ active: bool,
+ ) -> Result<(), gst::LoggableError> {
+ if let gst::PadMode::Push = mode {
+ if active {
+ self.state.lock().unwrap().flow_return = Ok(gst::FlowSuccess::Ok);
+ self.start_task(bwe)?;
+ } else {
+ let mut state = self.state.lock().unwrap();
+ state.flow_return = Err(gst::FlowError::Flushing);
+ drop(state);
+
+ self.srcpad.stop_task()?;
+ }
+
+ Ok(())
+ } else {
+ Err(gst::LoggableError::new(
+ *CAT,
+ glib::bool_error!("Unsupported pad mode {mode:?}"),
+ ))
+ }
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for BandwidthEstimator {
+ const NAME: &'static str = "GstRTPGCCBwE";
+ type Type = super::BandwidthEstimator;
+ type ParentType = gst::Element;
+
+ fn with_class(klass: &Self::Class) -> Self {
+ let templ = klass.pad_template("sink").unwrap();
+ let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink"))
+ .chain_function(|_pad, parent, mut buffer| {
+ BandwidthEstimator::catch_panic_pad_function(
+ parent,
+ || Err(gst::FlowError::Error),
+ |this| {
+ let mut state = this.state.lock().unwrap();
+ let mutbuf = buffer.make_mut();
+ mutbuf.set_pts(None);
+ mutbuf.set_dts(None);
+ state.buffers.push_front(buffer);
+
+ state.flow_return
+ },
+ )
+ })
+ .flags(gst::PadFlags::PROXY_CAPS | gst::PadFlags::PROXY_ALLOCATION)
+ .build();
+
+ let templ = klass.pad_template("src").unwrap();
+ let srcpad = gst::Pad::builder_with_template(&templ, Some("src"))
+ .event_function(|pad, parent, event| {
+ BandwidthEstimator::catch_panic_pad_function(
+ parent,
+ || false,
+ |this| {
+ let bwe = this.instance();
+
+ if let Some(structure) = event.structure() {
+ if structure.name() == "RTPTWCCPackets" {
+ let varray = structure.get::<glib::ValueArray>("packets").unwrap();
+ let mut packets = varray
+ .iter()
+ .filter_map(|s| {
+ Packet::from_structure(&s.get::<gst::Structure>().unwrap())
+ })
+ .collect::<Vec<Packet>>();
+
+ let bitrate_changed = {
+ let mut state = this.state.lock().unwrap();
+
+ state.detector.update(&mut packets);
+ if !state.delay_control(&bwe) {
+ state.loss_control(&bwe)
+ } else {
+ true
+ }
+ };
+
+ if bitrate_changed {
+ bwe.notify("estimated-bitrate")
+ }
+ }
+ }
+
+ gst::Pad::event_default(pad, parent, event)
+ },
+ )
+ })
+ .activatemode_function(|pad, parent, mode, active| {
+ BandwidthEstimator::catch_panic_pad_function(
+ parent,
+ || {
+ Err(gst::loggable_error!(
+ CAT,
+ "Panic activating src pad with mode"
+ ))
+ },
+ |this| this.src_activatemode(pad, &this.instance(), mode, active),
+ )
+ })
+ .flags(gst::PadFlags::PROXY_CAPS | gst::PadFlags::PROXY_ALLOCATION)
+ .build();
+
+ Self {
+ state: Default::default(),
+ srcpad,
+ sinkpad,
+ }
+ }
+}
+
+impl ObjectImpl for BandwidthEstimator {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ let obj = self.instance();
+ obj.add_pad(&self.sinkpad).unwrap();
+ obj.add_pad(&self.srcpad).unwrap();
+ }
+
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ /*
+ * gcc:estimated-bitrate:
+ *
+ * Currently computed network bitrate, should be used
+ * to set encoders bitrate.
+ */
+ glib::ParamSpecUInt::new(
+ "estimated-bitrate",
+ "Estimated Bitrate",
+ "Currently estimated bitrate. Can be set before starting
+ the element to configure the starting bitrate, in which case the
+ encoder should also use it as target bitrate",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_MIN_BITRATE as u32,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecUInt::new(
+ "min-bitrate",
+ "Minimal Bitrate",
+ "Minimal bitrate to use (in bit/sec) when computing it through the bandwidth estimation algorithm",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_MIN_BITRATE,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecUInt::new(
+ "max-bitrate",
+ "Maximal Bitrate",
+ "Maximal bitrate to use (in bit/sec) when computing it through the bandwidth estimation algorithm",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_MAX_BITRATE,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "min-bitrate" => {
+ let mut state = self.state.lock().unwrap();
+ state.min_bitrate = value.get::<u32>().expect("type checked upstream");
+ }
+ "max-bitrate" => {
+ let mut state = self.state.lock().unwrap();
+ state.max_bitrate = value.get::<u32>().expect("type checked upstream");
+ }
+ "estimated-bitrate" => {
+ let mut state = self.state.lock().unwrap();
+ let bitrate = value.get::<u32>().expect("type checked upstream");
+ state.target_bitrate_on_delay = bitrate;
+ state.target_bitrate_on_loss = bitrate;
+ state.estimated_bitrate = bitrate;
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "min-bitrate" => {
+ let state = self.state.lock().unwrap();
+ state.min_bitrate.to_value()
+ }
+ "max-bitrate" => {
+ let state = self.state.lock().unwrap();
+ state.max_bitrate.to_value()
+ }
+ "estimated-bitrate" => {
+ let state = self.state.lock().unwrap();
+ state.estimated_bitrate.to_value()
+ }
+ _ => unimplemented!(),
+ }
+ }
+}
+
+impl GstObjectImpl for BandwidthEstimator {}
+
+impl ElementImpl for BandwidthEstimator {
+ fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
+ static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
+ gst::subclass::ElementMetadata::new(
+ "Google Congestion Control bandwidth estimator",
+ "Network/WebRTC/RTP/Filter",
+ "Estimates current network bandwidth using the Google Congestion Control algorithm \
+ notifying about it through the 'bitrate' property",
+ "Thibault Saunier <tsaunier@igalia.com>",
+ )
+ });
+
+ Some(&*ELEMENT_METADATA)
+ }
+
+ fn pad_templates() -> &'static [gst::PadTemplate] {
+ static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
+ let caps = gst::Caps::builder_full()
+ .structure(gst::Structure::builder("application/x-rtp").build())
+ .build();
+
+ let sinkpad_template = gst::PadTemplate::new(
+ "sink",
+ gst::PadDirection::Sink,
+ gst::PadPresence::Always,
+ &caps,
+ )
+ .unwrap();
+
+ let srcpad_template = gst::PadTemplate::new(
+ "src",
+ gst::PadDirection::Src,
+ gst::PadPresence::Always,
+ &caps,
+ )
+ .unwrap();
+
+ vec![sinkpad_template, srcpad_template]
+ });
+
+ PAD_TEMPLATES.as_ref()
+ }
+}
diff --git a/net/webrtc/plugins/src/gcc/mod.rs b/net/webrtc/plugins/src/gcc/mod.rs
new file mode 100644
index 00000000..5bbc07db
--- /dev/null
+++ b/net/webrtc/plugins/src/gcc/mod.rs
@@ -0,0 +1,16 @@
+use gst::glib;
+use gst::prelude::*;
+mod imp;
+
+glib::wrapper! {
+ pub struct BandwidthEstimator(ObjectSubclass<imp::BandwidthEstimator>) @extends gst::Element, gst::Object;
+}
+
+pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ gst::Element::register(
+ Some(plugin),
+ "rtpgccbwe",
+ gst::Rank::None,
+ BandwidthEstimator::static_type(),
+ )
+}
diff --git a/net/webrtc/plugins/src/lib.rs b/net/webrtc/plugins/src/lib.rs
new file mode 100644
index 00000000..81a65cea
--- /dev/null
+++ b/net/webrtc/plugins/src/lib.rs
@@ -0,0 +1,24 @@
+use gst::glib;
+
+pub mod gcc;
+mod signaller;
+pub mod webrtcsink;
+
+fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ webrtcsink::register(plugin)?;
+ gcc::register(plugin)?;
+
+ Ok(())
+}
+
+gst::plugin_define!(
+ webrtcsink,
+ env!("CARGO_PKG_DESCRIPTION"),
+ plugin_init,
+ concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
+ "MPL-2.0",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_REPOSITORY"),
+ env!("BUILD_REL_DATE")
+);
diff --git a/net/webrtc/plugins/src/signaller/imp.rs b/net/webrtc/plugins/src/signaller/imp.rs
new file mode 100644
index 00000000..74415c4b
--- /dev/null
+++ b/net/webrtc/plugins/src/signaller/imp.rs
@@ -0,0 +1,478 @@
+use crate::webrtcsink::WebRTCSink;
+use anyhow::{anyhow, Error};
+use async_std::task;
+use async_tungstenite::tungstenite::Message as WsMessage;
+use futures::channel::mpsc;
+use futures::prelude::*;
+use gst::glib::prelude::*;
+use gst::glib::{self, Type};
+use gst::prelude::*;
+use gst::subclass::prelude::*;
+use once_cell::sync::Lazy;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::Mutex;
+use webrtcsink_protocol as p;
+
+static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "webrtcsink-signaller",
+ gst::DebugColorFlags::empty(),
+ Some("WebRTC sink signaller"),
+ )
+});
+
+#[derive(Default)]
+struct State {
+ /// Sender for the websocket messages
+ websocket_sender: Option<mpsc::Sender<p::IncomingMessage>>,
+ send_task_handle: Option<task::JoinHandle<Result<(), Error>>>,
+ receive_task_handle: Option<task::JoinHandle<()>>,
+}
+
+#[derive(Clone)]
+struct Settings {
+ address: Option<String>,
+ cafile: Option<PathBuf>,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ address: Some("ws://127.0.0.1:8443".to_string()),
+ cafile: None,
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct Signaller {
+ state: Mutex<State>,
+ settings: Mutex<Settings>,
+}
+
+impl Signaller {
+ async fn connect(&self, element: &WebRTCSink) -> Result<(), Error> {
+ let settings = self.settings.lock().unwrap().clone();
+
+ let connector = if let Some(path) = settings.cafile {
+ let cert = async_std::fs::read_to_string(&path).await?;
+ let cert = async_native_tls::Certificate::from_pem(cert.as_bytes())?;
+ let connector = async_native_tls::TlsConnector::new();
+ Some(connector.add_root_certificate(cert))
+ } else {
+ None
+ };
+
+ let (ws, _) = async_tungstenite::async_std::connect_async_with_tls_connector(
+ settings.address.unwrap(),
+ connector,
+ )
+ .await?;
+
+ gst::info!(CAT, obj: element, "connected");
+
+ // Channel for asynchronously sending out websocket message
+ let (mut ws_sink, mut ws_stream) = ws.split();
+
+ // 1000 is completely arbitrary, we simply don't want infinite piling
+ // up of messages as with unbounded
+ let (mut websocket_sender, mut websocket_receiver) =
+ mpsc::channel::<p::IncomingMessage>(1000);
+ let element_clone = element.downgrade();
+ let send_task_handle = task::spawn(async move {
+ while let Some(msg) = websocket_receiver.next().await {
+ if let Some(element) = element_clone.upgrade() {
+ gst::trace!(CAT, obj: &element, "Sending websocket message {:?}", msg);
+ }
+ ws_sink
+ .send(WsMessage::Text(serde_json::to_string(&msg).unwrap()))
+ .await?;
+ }
+
+ if let Some(element) = element_clone.upgrade() {
+ gst::info!(CAT, obj: &element, "Done sending");
+ }
+
+ ws_sink.send(WsMessage::Close(None)).await?;
+ ws_sink.close().await?;
+
+ Ok::<(), Error>(())
+ });
+
+ let meta = if let Some(meta) = element.property::<Option<gst::Structure>>("meta") {
+ serialize_value(&meta.to_value())
+ } else {
+ None
+ };
+ websocket_sender
+ .send(p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta,
+ peer_id: None,
+ }))
+ .await?;
+
+ let element_clone = element.downgrade();
+ let receive_task_handle = task::spawn(async move {
+ while let Some(msg) = async_std::stream::StreamExt::next(&mut ws_stream).await {
+ if let Some(element) = element_clone.upgrade() {
+ match msg {
+ Ok(WsMessage::Text(msg)) => {
+ gst::trace!(CAT, obj: &element, "Received message {}", msg);
+
+ if let Ok(msg) = serde_json::from_str::<p::OutgoingMessage>(&msg) {
+ match msg {
+ p::OutgoingMessage::Welcome { peer_id } => {
+ gst::info!(
+ CAT,
+ obj: &element,
+ "We are registered with the server, our peer id is {}",
+ peer_id
+ );
+ }
+ p::OutgoingMessage::StartSession {
+ session_id,
+ peer_id,
+ } => {
+ if let Err(err) =
+ element.start_session(&session_id, &peer_id)
+ {
+ gst::warning!(CAT, obj: &element, "{}", err);
+ }
+ }
+ p::OutgoingMessage::EndSession(session_info) => {
+ if let Err(err) =
+ element.end_session(&session_info.session_id)
+ {
+ gst::warning!(CAT, obj: &element, "{}", err);
+ }
+ }
+ p::OutgoingMessage::Peer(p::PeerMessage {
+ session_id,
+ peer_message,
+ }) => match peer_message {
+ p::PeerMessageInner::Sdp(p::SdpMessage::Answer { sdp }) => {
+ if let Err(err) = element.handle_sdp(
+ &session_id,
+ &gst_webrtc::WebRTCSessionDescription::new(
+ gst_webrtc::WebRTCSDPType::Answer,
+ gst_sdp::SDPMessage::parse_buffer(
+ sdp.as_bytes(),
+ )
+ .unwrap(),
+ ),
+ ) {
+ gst::warning!(CAT, obj: &element, "{}", err);
+ }
+ }
+ p::PeerMessageInner::Sdp(p::SdpMessage::Offer {
+ ..
+ }) => {
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Ignoring offer from peer"
+ );
+ }
+ p::PeerMessageInner::Ice {
+ candidate,
+ sdp_m_line_index,
+ } => {
+ if let Err(err) = element.handle_ice(
+ &session_id,
+ Some(sdp_m_line_index),
+ None,
+ &candidate,
+ ) {
+ gst::warning!(CAT, obj: &element, "{}", err);
+ }
+ }
+ },
+ _ => {
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Ignoring unsupported message {:?}",
+ msg
+ );
+ }
+ }
+ } else {
+ gst::error!(
+ CAT,
+ obj: &element,
+ "Unknown message from server: {}",
+ msg
+ );
+ element.handle_signalling_error(
+ anyhow!("Unknown message from server: {}", msg).into(),
+ );
+ }
+ }
+ Ok(WsMessage::Close(reason)) => {
+ gst::info!(
+ CAT,
+ obj: &element,
+ "websocket connection closed: {:?}",
+ reason
+ );
+ break;
+ }
+ Ok(_) => (),
+ Err(err) => {
+ element.handle_signalling_error(
+ anyhow!("Error receiving: {}", err).into(),
+ );
+ break;
+ }
+ }
+ } else {
+ break;
+ }
+ }
+
+ if let Some(element) = element_clone.upgrade() {
+ gst::info!(CAT, obj: &element, "Stopped websocket receiving");
+ }
+ });
+
+ let mut state = self.state.lock().unwrap();
+ state.websocket_sender = Some(websocket_sender);
+ state.send_task_handle = Some(send_task_handle);
+ state.receive_task_handle = Some(receive_task_handle);
+
+ Ok(())
+ }
+
+ pub fn start(&self, element: &WebRTCSink) {
+ let this = self.instance().clone();
+ let element_clone = element.clone();
+ task::spawn(async move {
+ let this = Self::from_instance(&this);
+ if let Err(err) = this.connect(&element_clone).await {
+ element_clone.handle_signalling_error(err.into());
+ }
+ });
+ }
+
+ pub fn handle_sdp(
+ &self,
+ element: &WebRTCSink,
+ session_id: &str,
+ sdp: &gst_webrtc::WebRTCSessionDescription,
+ ) {
+ let state = self.state.lock().unwrap();
+
+ let msg = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id: session_id.to_string(),
+ peer_message: p::PeerMessageInner::Sdp(p::SdpMessage::Offer {
+ sdp: sdp.sdp().as_text().unwrap(),
+ }),
+ });
+
+ if let Some(mut sender) = state.websocket_sender.clone() {
+ let element = element.downgrade();
+ task::spawn(async move {
+ if let Err(err) = sender.send(msg).await {
+ if let Some(element) = element.upgrade() {
+ element.handle_signalling_error(anyhow!("Error: {}", err).into());
+ }
+ }
+ });
+ }
+ }
+
+ pub fn handle_ice(
+ &self,
+ element: &WebRTCSink,
+ session_id: &str,
+ candidate: &str,
+ sdp_m_line_index: Option<u32>,
+ _sdp_mid: Option<String>,
+ ) {
+ let state = self.state.lock().unwrap();
+
+ let msg = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id: session_id.to_string(),
+ peer_message: p::PeerMessageInner::Ice {
+ candidate: candidate.to_string(),
+ sdp_m_line_index: sdp_m_line_index.unwrap(),
+ },
+ });
+
+ if let Some(mut sender) = state.websocket_sender.clone() {
+ let element = element.downgrade();
+ task::spawn(async move {
+ if let Err(err) = sender.send(msg).await {
+ if let Some(element) = element.upgrade() {
+ element.handle_signalling_error(anyhow!("Error: {}", err).into());
+ }
+ }
+ });
+ }
+ }
+
+ pub fn stop(&self, element: &WebRTCSink) {
+ gst::info!(CAT, obj: element, "Stopping now");
+
+ let mut state = self.state.lock().unwrap();
+ let send_task_handle = state.send_task_handle.take();
+ let receive_task_handle = state.receive_task_handle.take();
+ if let Some(mut sender) = state.websocket_sender.take() {
+ task::block_on(async move {
+ sender.close_channel();
+
+ if let Some(handle) = send_task_handle {
+ if let Err(err) = handle.await {
+ gst::warning!(CAT, obj: element, "Error while joining send task: {}", err);
+ }
+ }
+
+ if let Some(handle) = receive_task_handle {
+ handle.await;
+ }
+ });
+ }
+ }
+
+ pub fn end_session(&self, element: &WebRTCSink, session_id: &str) {
+ gst::debug!(CAT, obj: element, "Signalling session {} ended", session_id);
+
+ let state = self.state.lock().unwrap();
+ let session_id = session_id.to_string();
+ let element = element.downgrade();
+ if let Some(mut sender) = state.websocket_sender.clone() {
+ task::spawn(async move {
+ if let Err(err) = sender
+ .send(p::IncomingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.to_string(),
+ }))
+ .await
+ {
+ if let Some(element) = element.upgrade() {
+ element.handle_signalling_error(anyhow!("Error: {}", err).into());
+ }
+ }
+ });
+ }
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for Signaller {
+ const NAME: &'static str = "RsWebRTCSinkSignaller";
+ type Type = super::Signaller;
+ type ParentType = glib::Object;
+}
+
+impl ObjectImpl for Signaller {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecString::new(
+ "address",
+ "Address",
+ "Address of the signalling server",
+ Some("ws://127.0.0.1:8443"),
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpecString::new(
+ "cafile",
+ "CA file",
+ "Path to a Certificate file to add to the set of roots the TLS connector will trust",
+ None,
+ glib::ParamFlags::READWRITE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "address" => {
+ let address: Option<_> = value.get().expect("type checked upstream");
+
+ if let Some(address) = address {
+ gst::info!(CAT, "Signaller address set to {}", address);
+
+ let mut settings = self.settings.lock().unwrap();
+ settings.address = Some(address);
+ } else {
+ gst::error!(CAT, "address can't be None");
+ }
+ }
+ "cafile" => {
+ let value: String = value.get().unwrap();
+ let mut settings = self.settings.lock().unwrap();
+ settings.cafile = Some(value.into());
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "address" => self.settings.lock().unwrap().address.to_value(),
+ "cafile" => {
+ let settings = self.settings.lock().unwrap();
+ let cafile = settings.cafile.as_ref();
+ cafile.and_then(|file| file.to_str()).to_value()
+ }
+ _ => unimplemented!(),
+ }
+ }
+}
+
+fn serialize_value(val: &gst::glib::Value) -> Option<serde_json::Value> {
+ match val.type_() {
+ Type::STRING => Some(val.get::<String>().unwrap().into()),
+ Type::BOOL => Some(val.get::<bool>().unwrap().into()),
+ Type::I32 => Some(val.get::<i32>().unwrap().into()),
+ Type::U32 => Some(val.get::<u32>().unwrap().into()),
+ Type::I_LONG | Type::I64 => Some(val.get::<i64>().unwrap().into()),
+ Type::U_LONG | Type::U64 => Some(val.get::<u64>().unwrap().into()),
+ Type::F32 => Some(val.get::<f32>().unwrap().into()),
+ Type::F64 => Some(val.get::<f64>().unwrap().into()),
+ _ => {
+ if let Ok(s) = val.get::<gst::Structure>() {
+ serde_json::to_value(
+ s.iter()
+ .filter_map(|(name, value)| {
+ serialize_value(value).map(|value| (name.to_string(), value))
+ })
+ .collect::<HashMap<String, serde_json::Value>>(),
+ )
+ .ok()
+ } else if let Ok(a) = val.get::<gst::Array>() {
+ serde_json::to_value(
+ a.iter()
+ .filter_map(|value| serialize_value(value))
+ .collect::<Vec<serde_json::Value>>(),
+ )
+ .ok()
+ } else if let Some((_klass, values)) = gst::glib::FlagsValue::from_value(val) {
+ Some(
+ values
+ .iter()
+ .map(|value| value.nick())
+ .collect::<Vec<&str>>()
+ .join("+")
+ .into(),
+ )
+ } else if let Ok(value) = val.serialize() {
+ Some(value.as_str().into())
+ } else {
+ gst::warning!(CAT, "Can't convert {} to json", val.type_().name());
+ None
+ }
+ }
+ }
+}
diff --git a/net/webrtc/plugins/src/signaller/mod.rs b/net/webrtc/plugins/src/signaller/mod.rs
new file mode 100644
index 00000000..51374254
--- /dev/null
+++ b/net/webrtc/plugins/src/signaller/mod.rs
@@ -0,0 +1,62 @@
+use crate::webrtcsink::{Signallable, WebRTCSink};
+use gst::glib;
+use gst::subclass::prelude::ObjectSubclassExt;
+use std::error::Error;
+
+mod imp;
+
+glib::wrapper! {
+ pub struct Signaller(ObjectSubclass<imp::Signaller>);
+}
+
+unsafe impl Send for Signaller {}
+unsafe impl Sync for Signaller {}
+
+impl Signallable for Signaller {
+ fn start(&mut self, element: &WebRTCSink) -> Result<(), Box<dyn Error>> {
+ let signaller = imp::Signaller::from_instance(self);
+ signaller.start(element);
+
+ Ok(())
+ }
+
+ fn handle_sdp(
+ &mut self,
+ element: &WebRTCSink,
+ peer_id: &str,
+ sdp: &gst_webrtc::WebRTCSessionDescription,
+ ) -> Result<(), Box<dyn Error>> {
+ let signaller = imp::Signaller::from_instance(self);
+ signaller.handle_sdp(element, peer_id, sdp);
+ Ok(())
+ }
+
+ fn handle_ice(
+ &mut self,
+ element: &WebRTCSink,
+ session_id: &str,
+ candidate: &str,
+ sdp_mline_index: Option<u32>,
+ sdp_mid: Option<String>,
+ ) -> Result<(), Box<dyn Error>> {
+ let signaller = imp::Signaller::from_instance(self);
+ signaller.handle_ice(element, session_id, candidate, sdp_mline_index, sdp_mid);
+ Ok(())
+ }
+
+ fn stop(&mut self, element: &WebRTCSink) {
+ let signaller = imp::Signaller::from_instance(self);
+ signaller.stop(element);
+ }
+
+ fn session_ended(&mut self, element: &WebRTCSink, session_id: &str) {
+ let signaller = imp::Signaller::from_instance(self);
+ signaller.end_session(element, session_id);
+ }
+}
+
+impl Default for Signaller {
+ fn default() -> Self {
+ glib::Object::new::<Self>(&[])
+ }
+}
diff --git a/net/webrtc/plugins/src/webrtcsink/homegrown_cc.rs b/net/webrtc/plugins/src/webrtcsink/homegrown_cc.rs
new file mode 100644
index 00000000..04b5258f
--- /dev/null
+++ b/net/webrtc/plugins/src/webrtcsink/homegrown_cc.rs
@@ -0,0 +1,420 @@
+use gst::{
+ glib::{self, value::FromValue},
+ prelude::*,
+};
+use once_cell::sync::Lazy;
+
+use super::imp::VideoEncoder;
+
+static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "webrtcsink-homegrowncc",
+ gst::DebugColorFlags::empty(),
+ Some("WebRTC sink"),
+ )
+});
+
+#[derive(Debug)]
+enum IncreaseType {
+ /// Increase bitrate by value
+ Additive(f64),
+ /// Increase bitrate by factor
+ Multiplicative(f64),
+}
+
+#[derive(Debug, Clone, Copy)]
+enum ControllerType {
+ // Running the "delay-based controller"
+ Delay,
+ // Running the "loss based controller"
+ Loss,
+}
+
+#[derive(Debug)]
+enum CongestionControlOp {
+ /// Don't update target bitrate
+ Hold,
+ /// Decrease target bitrate
+ Decrease {
+ factor: f64,
+ #[allow(dead_code)]
+ reason: String, // for Debug
+ },
+ /// Increase target bitrate, either additively or multiplicatively
+ Increase(IncreaseType),
+}
+
+fn lookup_twcc_stats(stats: &gst::StructureRef) -> Option<gst::Structure> {
+ for (_, field_value) in stats {
+ if let Ok(s) = field_value.get::<gst::Structure>() {
+ if let Ok(type_) = s.get::<gst_webrtc::WebRTCStatsType>("type") {
+ if (type_ == gst_webrtc::WebRTCStatsType::Transport
+ || type_ == gst_webrtc::WebRTCStatsType::CandidatePair)
+ && s.has_field("gst-twcc-stats")
+ {
+ return Some(s.get::<gst::Structure>("gst-twcc-stats").unwrap());
+ }
+ }
+ }
+ }
+
+ None
+}
+
+pub struct CongestionController {
+ /// Note: The target bitrate applied is the min of
+ /// target_bitrate_on_delay and target_bitrate_on_loss
+ ///
+ /// Bitrate target based on delay factor for all video streams.
+ /// Hasn't been tested with multiple video streams, but
+ /// current design is simply to divide bitrate equally.
+ pub target_bitrate_on_delay: i32,
+
+ /// Bitrate target based on loss for all video streams.
+ pub target_bitrate_on_loss: i32,
+
+ /// Exponential moving average, updated when bitrate is
+ /// decreased, discarded when increased again past last
+ /// congestion window. Smoothing factor hardcoded.
+ bitrate_ema: Option<f64>,
+ /// Exponentially weighted moving variance, recursively
+ /// updated along with bitrate_ema. sqrt'd to obtain standard
+ /// deviation, used to determine whether to increase bitrate
+ /// additively or multiplicatively
+ bitrate_emvar: f64,
+ /// Used in additive mode to track last control time, influences
+ /// calculation of added value according to gcc section 5.5
+ last_update_time: Option<std::time::Instant>,
+ /// For logging purposes
+ peer_id: String,
+
+ min_bitrate: u32,
+ max_bitrate: u32,
+}
+
+impl CongestionController {
+ pub fn new(peer_id: &str, min_bitrate: u32, max_bitrate: u32) -> Self {
+ Self {
+ target_bitrate_on_delay: 0,
+ target_bitrate_on_loss: 0,
+ bitrate_ema: None,
+ bitrate_emvar: 0.,
+ last_update_time: None,
+ peer_id: peer_id.to_string(),
+ min_bitrate,
+ max_bitrate,
+ }
+ }
+
+ fn update_delay(
+ &mut self,
+ element: &super::WebRTCSink,
+ twcc_stats: &gst::StructureRef,
+ rtt: f64,
+ ) -> CongestionControlOp {
+ let target_bitrate = f64::min(
+ self.target_bitrate_on_delay as f64,
+ self.target_bitrate_on_loss as f64,
+ );
+ // Unwrap, all those fields must be there or there's been an API
+ // break, which qualifies as programming error
+ let bitrate_sent = twcc_stats.get::<u32>("bitrate-sent").unwrap();
+ let bitrate_recv = twcc_stats.get::<u32>("bitrate-recv").unwrap();
+ let delta_of_delta = twcc_stats.get::<i64>("avg-delta-of-delta").unwrap();
+
+ let sent_minus_received = bitrate_sent.saturating_sub(bitrate_recv);
+
+ let delay_factor = sent_minus_received as f64 / target_bitrate;
+ let last_update_time = self.last_update_time.replace(std::time::Instant::now());
+
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: considering stats {}",
+ self.peer_id,
+ twcc_stats
+ );
+
+ if delay_factor > 0.1 {
+ let (factor, reason) = if delay_factor < 0.64 {
+ (0.96, format!("low delay factor {}", delay_factor))
+ } else {
+ (
+ delay_factor.sqrt().sqrt().clamp(0.8, 0.96),
+ format!("High delay factor {}", delay_factor),
+ )
+ };
+
+ CongestionControlOp::Decrease { factor, reason }
+ } else if delta_of_delta > 1_000_000 {
+ CongestionControlOp::Decrease {
+ factor: 0.97,
+ reason: format!("High delta: {}", delta_of_delta),
+ }
+ } else {
+ CongestionControlOp::Increase(if let Some(ema) = self.bitrate_ema {
+ let bitrate_stdev = self.bitrate_emvar.sqrt();
+
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: Old bitrate: {}, ema: {}, stddev: {}",
+ self.peer_id,
+ target_bitrate,
+ ema,
+ bitrate_stdev,
+ );
+
+ // gcc section 5.5 advises 3 standard deviations, but experiments
+ // have shown this to be too low, probably related to the rest of
+ // homegrown algorithm not implementing gcc, revisit when implementing
+ // the rest of the RFC
+ if target_bitrate < ema - 7. * bitrate_stdev {
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: below last congestion window",
+ self.peer_id
+ );
+ /* Multiplicative increase */
+ IncreaseType::Multiplicative(1.03)
+ } else if target_bitrate > ema + 7. * bitrate_stdev {
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: above last congestion window",
+ self.peer_id
+ );
+ /* We have gone past our last estimated max bandwidth
+ * network situation may have changed, go back to
+ * multiplicative increase
+ */
+ self.bitrate_ema.take();
+ IncreaseType::Multiplicative(1.03)
+ } else {
+ let rtt_ms = rtt * 1000.;
+ let response_time_ms = 100. + rtt_ms;
+ let time_since_last_update_ms = match last_update_time {
+ None => 0.,
+ Some(instant) => {
+ (self.last_update_time.unwrap() - instant).as_millis() as f64
+ }
+ };
+ // gcc section 5.5 advises 0.95 as the smoothing factor, but that
+ // seems intuitively much too low, granting disproportionate importance
+ // to the last measurement. 0.5 seems plenty enough, I don't have maths
+ // to back that up though :)
+ let alpha = 0.5 * f64::min(time_since_last_update_ms / response_time_ms, 1.0);
+ let bits_per_frame = target_bitrate / 30.;
+ let packets_per_frame = f64::ceil(bits_per_frame / (1200. * 8.));
+ let avg_packet_size_bits = bits_per_frame / packets_per_frame;
+
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: still in last congestion window",
+ self.peer_id,
+ );
+
+ /* Additive increase */
+ IncreaseType::Additive(f64::max(1000., alpha * avg_packet_size_bits))
+ }
+ } else {
+ /* Multiplicative increase */
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: outside congestion window",
+ self.peer_id
+ );
+ IncreaseType::Multiplicative(1.03)
+ })
+ }
+ }
+
+ fn clamp_bitrate(&mut self, bitrate: i32, n_encoders: i32, controller_type: ControllerType) {
+ match controller_type {
+ ControllerType::Loss => {
+ self.target_bitrate_on_loss = bitrate.clamp(
+ self.min_bitrate as i32 * n_encoders,
+ self.max_bitrate as i32 * n_encoders,
+ )
+ }
+
+ ControllerType::Delay => {
+ self.target_bitrate_on_delay = bitrate.clamp(
+ self.min_bitrate as i32 * n_encoders,
+ self.max_bitrate as i32 * n_encoders,
+ )
+ }
+ }
+ }
+
+ fn get_remote_inbound_stats(&self, stats: &gst::StructureRef) -> Vec<gst::Structure> {
+ let mut inbound_rtp_stats: Vec<gst::Structure> = Default::default();
+ for (_, field_value) in stats {
+ if let Ok(s) = field_value.get::<gst::Structure>() {
+ if let Ok(type_) = s.get::<gst_webrtc::WebRTCStatsType>("type") {
+ if type_ == gst_webrtc::WebRTCStatsType::RemoteInboundRtp {
+ inbound_rtp_stats.push(s);
+ }
+ }
+ }
+ }
+
+ inbound_rtp_stats
+ }
+
+ fn lookup_rtt(&self, stats: &gst::StructureRef) -> f64 {
+ let inbound_rtp_stats = self.get_remote_inbound_stats(stats);
+ let mut rtt = 0.;
+ let mut n_rtts = 0u64;
+ for inbound_stat in &inbound_rtp_stats {
+ if let Err(err) = (|| -> Result<(), gst::structure::GetError<<<f64 as FromValue>::Checker as glib::value::ValueTypeChecker>::Error>> {
+ rtt += inbound_stat.get::<f64>("round-trip-time")?;
+ n_rtts += 1;
+
+ Ok(())
+ })() {
+ gst::debug!(CAT, "{:?}", err);
+ }
+ }
+
+ rtt /= f64::max(1., n_rtts as f64);
+
+ gst::log!(CAT, "Round trip time: {}", rtt);
+
+ rtt
+ }
+
+ pub fn loss_control(
+ &mut self,
+ element: &super::WebRTCSink,
+ stats: &gst::StructureRef,
+ encoders: &mut Vec<VideoEncoder>,
+ ) {
+ let loss_percentage = stats.get::<f64>("packet-loss-pct").unwrap();
+
+ self.apply_control_op(
+ element,
+ encoders,
+ if loss_percentage > 10. {
+ CongestionControlOp::Decrease {
+ factor: ((100. - (0.5 * loss_percentage)) / 100.).clamp(0.7, 0.98),
+ reason: format!("High loss: {}", loss_percentage),
+ }
+ } else if loss_percentage > 2. {
+ CongestionControlOp::Hold
+ } else {
+ CongestionControlOp::Increase(IncreaseType::Multiplicative(1.05))
+ },
+ ControllerType::Loss,
+ );
+ }
+
+ pub fn delay_control(
+ &mut self,
+ element: &super::WebRTCSink,
+ stats: &gst::StructureRef,
+ encoders: &mut Vec<VideoEncoder>,
+ ) {
+ if let Some(twcc_stats) = lookup_twcc_stats(stats) {
+ let op = self.update_delay(element, &twcc_stats, self.lookup_rtt(stats));
+ self.apply_control_op(element, encoders, op, ControllerType::Delay);
+ }
+ }
+
+ fn apply_control_op(
+ &mut self,
+ element: &super::WebRTCSink,
+ encoders: &mut Vec<VideoEncoder>,
+ control_op: CongestionControlOp,
+ controller_type: ControllerType,
+ ) {
+ gst::trace!(
+ CAT,
+ obj: element,
+ "consumer {}: applying congestion control operation {:?}",
+ self.peer_id,
+ control_op
+ );
+
+ let n_encoders = encoders.len() as i32;
+ let prev_bitrate = i32::min(self.target_bitrate_on_delay, self.target_bitrate_on_loss);
+ match &control_op {
+ CongestionControlOp::Hold => {}
+ CongestionControlOp::Increase(IncreaseType::Additive(value)) => {
+ self.clamp_bitrate(
+ self.target_bitrate_on_delay + *value as i32,
+ n_encoders,
+ controller_type,
+ );
+ }
+ CongestionControlOp::Increase(IncreaseType::Multiplicative(factor)) => {
+ self.clamp_bitrate(
+ (self.target_bitrate_on_delay as f64 * factor) as i32,
+ n_encoders,
+ controller_type,
+ );
+ }
+ CongestionControlOp::Decrease { factor, .. } => {
+ self.clamp_bitrate(
+ (self.target_bitrate_on_delay as f64 * factor) as i32,
+ n_encoders,
+ controller_type,
+ );
+
+ if let ControllerType::Delay = controller_type {
+ // Smoothing factor
+ let alpha = 0.75;
+ if let Some(ema) = self.bitrate_ema {
+ let sigma: f64 = (self.target_bitrate_on_delay as f64) - ema;
+ self.bitrate_ema = Some(ema + (alpha * sigma));
+ self.bitrate_emvar =
+ (1. - alpha) * (self.bitrate_emvar + alpha * sigma.powi(2));
+ } else {
+ self.bitrate_ema = Some(self.target_bitrate_on_delay as f64);
+ self.bitrate_emvar = 0.;
+ }
+ }
+ }
+ }
+
+ let target_bitrate =
+ i32::min(self.target_bitrate_on_delay, self.target_bitrate_on_loss).clamp(
+ self.min_bitrate as i32 * n_encoders,
+ self.max_bitrate as i32 * n_encoders,
+ ) / n_encoders;
+
+ if target_bitrate != prev_bitrate {
+ gst::info!(
+ CAT,
+ "{:?} {} => {} | on delay {} - on loss {} | min {} - max {}",
+ control_op,
+ human_bytes::human_bytes(prev_bitrate),
+ human_bytes::human_bytes(target_bitrate),
+ human_bytes::human_bytes(self.target_bitrate_on_delay),
+ human_bytes::human_bytes(self.target_bitrate_on_loss),
+ human_bytes::human_bytes(self.min_bitrate),
+ human_bytes::human_bytes(self.max_bitrate),
+ );
+ }
+
+ let fec_ratio = {
+ if target_bitrate <= 2000000 || self.max_bitrate <= 2000000 {
+ 0f64
+ } else {
+ (target_bitrate as f64 - 2000000f64) / (self.max_bitrate as f64 - 2000000f64)
+ }
+ };
+
+ let fec_percentage = (fec_ratio * 50f64) as u32;
+
+ for encoder in encoders.iter_mut() {
+ encoder.set_bitrate(element, target_bitrate);
+ encoder
+ .transceiver
+ .set_property("fec-percentage", fec_percentage);
+ }
+ }
+}
diff --git a/net/webrtc/plugins/src/webrtcsink/imp.rs b/net/webrtc/plugins/src/webrtcsink/imp.rs
new file mode 100644
index 00000000..07f773a6
--- /dev/null
+++ b/net/webrtc/plugins/src/webrtcsink/imp.rs
@@ -0,0 +1,2852 @@
+use anyhow::Context;
+use gst::glib;
+use gst::prelude::*;
+use gst::subclass::prelude::*;
+use gst_rtp::prelude::*;
+use gst_utils::StreamProducer;
+use gst_video::subclass::prelude::*;
+use gst_webrtc::WebRTCDataChannel;
+
+use async_std::task;
+use futures::prelude::*;
+
+use anyhow::{anyhow, Error};
+use once_cell::sync::Lazy;
+use std::collections::HashMap;
+use std::ops::Mul;
+use std::sync::Mutex;
+
+use super::homegrown_cc::CongestionController;
+use super::{WebRTCSinkCongestionControl, WebRTCSinkError, WebRTCSinkMitigationMode};
+use crate::signaller::Signaller;
+use std::collections::BTreeMap;
+
+static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "webrtcsink",
+ gst::DebugColorFlags::empty(),
+ Some("WebRTC sink"),
+ )
+});
+
+const CUDA_MEMORY_FEATURE: &str = "memory:CUDAMemory";
+const GL_MEMORY_FEATURE: &str = "memory:GLMemory";
+const NVMM_MEMORY_FEATURE: &str = "memory:NVMM";
+
+const RTP_TWCC_URI: &str =
+ "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01";
+
+const DEFAULT_STUN_SERVER: Option<&str> = Some("stun://stun.l.google.com:19302");
+const DEFAULT_MIN_BITRATE: u32 = 1000;
+
+/* I have found higher values to cause packet loss *somewhere* in
+ * my local network, possibly related to chrome's pretty low UDP
+ * buffer sizes */
+const DEFAULT_MAX_BITRATE: u32 = 8192000;
+const DEFAULT_CONGESTION_CONTROL: WebRTCSinkCongestionControl =
+ WebRTCSinkCongestionControl::GoogleCongestionControl;
+const DEFAULT_DO_FEC: bool = true;
+const DEFAULT_DO_RETRANSMISSION: bool = true;
+const DEFAULT_ENABLE_DATA_CHANNEL_NAVIGATION: bool = false;
+const DEFAULT_START_BITRATE: u32 = 2048000;
+/* Start adding some FEC when the bitrate > 2Mbps as we found experimentally
+ * that it is not worth it below that threshold */
+const DO_FEC_THRESHOLD: u32 = 2000000;
+
+#[derive(Debug, Clone, Copy)]
+struct CCInfo {
+ heuristic: WebRTCSinkCongestionControl,
+ min_bitrate: u32,
+ max_bitrate: u32,
+ start_bitrate: u32,
+}
+
+/// User configuration
+struct Settings {
+ video_caps: gst::Caps,
+ audio_caps: gst::Caps,
+ turn_server: Option<String>,
+ stun_server: Option<String>,
+ cc_info: CCInfo,
+ do_fec: bool,
+ do_retransmission: bool,
+ enable_data_channel_navigation: bool,
+ meta: Option<gst::Structure>,
+}
+
+/// Represents a codec we can offer
+#[derive(Debug, Clone)]
+struct Codec {
+ encoder: gst::ElementFactory,
+ payloader: gst::ElementFactory,
+ caps: gst::Caps,
+ payload: i32,
+}
+
+impl Codec {
+ fn is_video(&self) -> bool {
+ self.encoder
+ .has_type(gst::ElementFactoryType::VIDEO_ENCODER)
+ }
+}
+
+/// Wrapper around our sink pads
+#[derive(Debug, Clone)]
+struct InputStream {
+ sink_pad: gst::GhostPad,
+ producer: Option<StreamProducer>,
+ /// The (fixed) caps coming in
+ in_caps: Option<gst::Caps>,
+ /// The caps we will offer, as a set of fixed structures
+ out_caps: Option<gst::Caps>,
+ /// Pace input data
+ clocksync: Option<gst::Element>,
+}
+
+/// Wrapper around webrtcbin pads
+#[derive(Clone)]
+struct WebRTCPad {
+ pad: gst::Pad,
+ /// The (fixed) caps of the corresponding input stream
+ in_caps: gst::Caps,
+ /// The m= line index in the SDP
+ media_idx: u32,
+ ssrc: u32,
+ /// The name of the corresponding InputStream's sink_pad
+ stream_name: String,
+ /// The payload selected in the answer, None at first
+ payload: Option<i32>,
+}
+
+/// Wrapper around GStreamer encoder element, keeps track of factory
+/// name in order to provide a unified set / get bitrate API, also
+/// tracks a raw capsfilter used to resize / decimate the input video
+/// stream according to the bitrate, thresholds hardcoded for now
+pub struct VideoEncoder {
+ factory_name: String,
+ codec_name: String,
+ element: gst::Element,
+ filter: gst::Element,
+ halved_framerate: gst::Fraction,
+ video_info: gst_video::VideoInfo,
+ session_id: String,
+ mitigation_mode: WebRTCSinkMitigationMode,
+ pub transceiver: gst_webrtc::WebRTCRTPTransceiver,
+}
+
+struct Session {
+ id: String,
+
+ pipeline: gst::Pipeline,
+ webrtcbin: gst::Element,
+ rtprtxsend: Option<gst::Element>,
+ webrtc_pads: HashMap<u32, WebRTCPad>,
+ peer_id: String,
+ encoders: Vec<VideoEncoder>,
+
+ // Our Homegrown controller (if cc_info.heuristic == Homegrown)
+ congestion_controller: Option<CongestionController>,
+ // Our BandwidthEstimator (if cc_info.heuristic == GoogleCongestionControl)
+ rtpgccbwe: Option<gst::Element>,
+
+ sdp: Option<gst_sdp::SDPMessage>,
+ stats: gst::Structure,
+
+ cc_info: CCInfo,
+
+ links: HashMap<u32, gst_utils::ConsumptionLink>,
+ stats_sigid: Option<glib::SignalHandlerId>,
+}
+
+#[derive(PartialEq)]
+enum SignallerState {
+ Started,
+ Stopped,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct NavigationEvent {
+ mid: Option<String>,
+ #[serde(flatten)]
+ event: gst_video::NavigationEvent,
+}
+
+/* Our internal state */
+struct State {
+ signaller: Box<dyn super::SignallableObject>,
+ signaller_state: SignallerState,
+ sessions: HashMap<String, Session>,
+ codecs: BTreeMap<i32, Codec>,
+ /// Used to abort codec discovery
+ codecs_abort_handle: Option<futures::future::AbortHandle>,
+ /// Used to wait for the discovery task to fully stop
+ codecs_done_receiver: Option<futures::channel::oneshot::Receiver<()>>,
+ /// Used to determine whether we can start the signaller when going to Playing,
+ /// or whether we should wait
+ codec_discovery_done: bool,
+ audio_serial: u32,
+ video_serial: u32,
+ streams: HashMap<String, InputStream>,
+ navigation_handler: Option<NavigationEventHandler>,
+ mids: HashMap<String, String>,
+}
+
+fn create_navigation_event(sink: &super::WebRTCSink, msg: &str) {
+ let event: Result<NavigationEvent, _> = serde_json::from_str(msg);
+
+ if let Ok(event) = event {
+ gst::log!(CAT, obj: sink, "Processing navigation event: {:?}", event);
+
+ if let Some(mid) = event.mid {
+ let this = WebRTCSink::from_instance(sink);
+
+ let state = this.state.lock().unwrap();
+ if let Some(stream_name) = state.mids.get(&mid) {
+ if let Some(stream) = state.streams.get(stream_name) {
+ let event = gst::event::Navigation::new(event.event.structure());
+
+ if !stream.sink_pad.push_event(event.clone()) {
+ gst::info!(CAT, "Could not send event: {:?}", event);
+ }
+ }
+ }
+ } else {
+ let this = WebRTCSink::from_instance(sink);
+
+ let state = this.state.lock().unwrap();
+ let event = gst::event::Navigation::new(event.event.structure());
+ state.streams.iter().for_each(|(_, stream)| {
+ if stream.sink_pad.name().starts_with("video_") {
+ gst::log!(CAT, "Navigating to: {:?}", event);
+ if !stream.sink_pad.push_event(event.clone()) {
+ gst::info!(CAT, "Could not send event: {:?}", event);
+ }
+ }
+ });
+ }
+ } else {
+ gst::error!(CAT, "Invalid navigation event: {:?}", msg);
+ }
+}
+
+/// Wrapper around `gst::ElementFactory::make` with a better error
+/// message
+pub fn make_element(element: &str, name: Option<&str>) -> Result<gst::Element, Error> {
+ gst::ElementFactory::make(element, name)
+ .with_context(|| format!("Failed to make element {}", element))
+}
+
+/// Simple utility for tearing down a pipeline cleanly
+struct PipelineWrapper(gst::Pipeline);
+
+// Structure to generate GstNavigation event from a WebRTCDataChannel
+// This is simply used to hold references to the inner items.
+#[derive(Debug)]
+struct NavigationEventHandler((glib::SignalHandlerId, WebRTCDataChannel));
+
+/// Our instance structure
+#[derive(Default)]
+pub struct WebRTCSink {
+ state: Mutex<State>,
+ settings: Mutex<Settings>,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ video_caps: ["video/x-vp8", "video/x-h264", "video/x-vp9", "video/x-h265"]
+ .iter()
+ .map(|s| gst::Structure::new_empty(s))
+ .collect::<gst::Caps>(),
+ audio_caps: ["audio/x-opus"]
+ .iter()
+ .map(|s| gst::Structure::new_empty(s))
+ .collect::<gst::Caps>(),
+ stun_server: DEFAULT_STUN_SERVER.map(String::from),
+ turn_server: None,
+ cc_info: CCInfo {
+ heuristic: WebRTCSinkCongestionControl::GoogleCongestionControl,
+ min_bitrate: DEFAULT_MIN_BITRATE,
+ max_bitrate: DEFAULT_MAX_BITRATE as u32,
+ start_bitrate: DEFAULT_START_BITRATE,
+ },
+ do_fec: DEFAULT_DO_FEC,
+ do_retransmission: DEFAULT_DO_RETRANSMISSION,
+ enable_data_channel_navigation: DEFAULT_ENABLE_DATA_CHANNEL_NAVIGATION,
+ meta: None,
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ let signaller = Signaller::default();
+
+ Self {
+ signaller: Box::new(signaller),
+ signaller_state: SignallerState::Stopped,
+ sessions: HashMap::new(),
+ codecs: BTreeMap::new(),
+ codecs_abort_handle: None,
+ codecs_done_receiver: None,
+ codec_discovery_done: false,
+ audio_serial: 0,
+ video_serial: 0,
+ streams: HashMap::new(),
+ navigation_handler: None,
+ mids: HashMap::new(),
+ }
+ }
+}
+
+fn make_converter_for_video_caps(caps: &gst::Caps) -> Result<gst::Element, Error> {
+ assert!(caps.is_fixed());
+
+ let video_info = gst_video::VideoInfo::from_caps(&caps)?;
+
+ let ret = gst::Bin::new(None);
+
+ let (head, mut tail) = {
+ if let Some(feature) = caps.features(0) {
+ if feature.contains(CUDA_MEMORY_FEATURE) {
+ let cudaupload = make_element("cudaupload", None)?;
+ let cudaconvert = make_element("cudaconvert", None)?;
+ let cudascale = make_element("cudascale", None)?;
+
+ ret.add_many(&[&cudaupload, &cudaconvert, &cudascale])?;
+ gst::Element::link_many(&[&cudaupload, &cudaconvert, &cudascale])?;
+
+ (cudaupload, cudascale)
+ } else if feature.contains(GL_MEMORY_FEATURE) {
+ let glupload = make_element("glupload", None)?;
+ let glconvert = make_element("glcolorconvert", None)?;
+ let glscale = make_element("glcolorscale", None)?;
+
+ ret.add_many(&[&glupload, &glconvert, &glscale])?;
+ gst::Element::link_many(&[&glupload, &glconvert, &glscale])?;
+
+ (glupload, glscale)
+ } else if feature.contains(NVMM_MEMORY_FEATURE) {
+ let queue = make_element("queue", None)?;
+ let nvconvert = make_element("nvvideoconvert", None)?;
+ nvconvert.set_property("compute-hw", 0);
+ nvconvert.set_property("nvbuf-memory-type", 0);
+
+ ret.add_many(&[&queue, &nvconvert])?;
+ gst::Element::link_many(&[&queue, &nvconvert])?;
+
+ (queue, nvconvert)
+ } else {
+ let convert = make_element("videoconvert", None)?;
+ let scale = make_element("videoscale", None)?;
+
+ ret.add_many(&[&convert, &scale])?;
+ gst::Element::link_many(&[&convert, &scale])?;
+
+ (convert, scale)
+ }
+ } else {
+ let convert = make_element("videoconvert", None)?;
+ let scale = make_element("videoscale", None)?;
+
+ ret.add_many(&[&convert, &scale])?;
+ gst::Element::link_many(&[&convert, &scale])?;
+
+ (convert, scale)
+ }
+ };
+
+ ret.add_pad(
+ &gst::GhostPad::with_target(Some("sink"), &head.static_pad("sink").unwrap()).unwrap(),
+ )
+ .unwrap();
+
+ if video_info.fps().numer() != 0 {
+ let vrate = make_element("videorate", None)?;
+ vrate.set_property("drop-only", true);
+ vrate.set_property("skip-to-first", true);
+
+ ret.add(&vrate)?;
+ tail.link(&vrate)?;
+ tail = vrate;
+ }
+
+ ret.add_pad(
+ &gst::GhostPad::with_target(Some("src"), &tail.static_pad("src").unwrap()).unwrap(),
+ )
+ .unwrap();
+
+ Ok(ret.upcast())
+}
+
+/// Default configuration for known encoders, can be disabled
+/// by returning True from an encoder-setup handler.
+fn configure_encoder(enc: &gst::Element, start_bitrate: u32) {
+ if let Some(factory) = enc.factory() {
+ match factory.name().as_str() {
+ "vp8enc" | "vp9enc" => {
+ enc.set_property("deadline", 1i64);
+ enc.set_property("target-bitrate", start_bitrate as i32);
+ enc.set_property("cpu-used", -16i32);
+ enc.set_property("keyframe-max-dist", 2000i32);
+ enc.set_property_from_str("keyframe-mode", "disabled");
+ enc.set_property_from_str("end-usage", "cbr");
+ enc.set_property("buffer-initial-size", 100i32);
+ enc.set_property("buffer-optimal-size", 120i32);
+ enc.set_property("buffer-size", 150i32);
+ enc.set_property("max-intra-bitrate", 250i32);
+ enc.set_property_from_str("error-resilient", "default");
+ enc.set_property("lag-in-frames", 0i32);
+ }
+ "x264enc" => {
+ enc.set_property("bitrate", start_bitrate / 1000);
+ enc.set_property_from_str("tune", "zerolatency");
+ enc.set_property_from_str("speed-preset", "ultrafast");
+ enc.set_property("threads", 12u32);
+ enc.set_property("key-int-max", 2560u32);
+ enc.set_property("b-adapt", false);
+ enc.set_property("vbv-buf-capacity", 120u32);
+ }
+ "nvh264enc" => {
+ enc.set_property("bitrate", start_bitrate / 1000);
+ enc.set_property("gop-size", 2560i32);
+ enc.set_property_from_str("rc-mode", "cbr-ld-hq");
+ enc.set_property("zerolatency", true);
+ }
+ "vaapih264enc" | "vaapivp8enc" => {
+ enc.set_property("bitrate", start_bitrate / 1000);
+ enc.set_property("keyframe-period", 2560u32);
+ enc.set_property_from_str("rate-control", "cbr");
+ }
+ "nvv4l2h264enc" => {
+ enc.set_property("bitrate", start_bitrate);
+ enc.set_property_from_str("preset-level", "UltraFastPreset");
+ enc.set_property("maxperf-enable", true);
+ enc.set_property("insert-vui", true);
+ enc.set_property("idrinterval", 256u32);
+ enc.set_property("insert-sps-pps", true);
+ enc.set_property("insert-aud", true);
+ enc.set_property_from_str("control-rate", "constant_bitrate");
+ }
+ "nvv4l2vp8enc" => {
+ enc.set_property("bitrate", start_bitrate);
+ enc.set_property_from_str("preset-level", "UltraFastPreset");
+ enc.set_property("maxperf-enable", true);
+ enc.set_property("idrinterval", 256u32);
+ enc.set_property_from_str("control-rate", "constant_bitrate");
+ }
+ _ => (),
+ }
+ }
+}
+
+/// Bit of an awkward function, but the goal here is to keep
+/// most of the encoding code for consumers in line with
+/// the codec discovery code, and this gets the job done.
+fn setup_encoding(
+ pipeline: &gst::Pipeline,
+ src: &gst::Element,
+ input_caps: &gst::Caps,
+ codec: &Codec,
+ ssrc: Option<u32>,
+ twcc: bool,
+) -> Result<(gst::Element, gst::Element, gst::Element), Error> {
+ let conv = match codec.is_video() {
+ true => make_converter_for_video_caps(input_caps)?.upcast(),
+ false => gst::parse_bin_from_description("audioresample ! audioconvert", true)?.upcast(),
+ };
+
+ let conv_filter = make_element("capsfilter", None)?;
+
+ let enc = codec
+ .encoder
+ .create(None)
+ .with_context(|| format!("Creating encoder {}", codec.encoder.name()))?;
+ let pay = codec
+ .payloader
+ .create(None)
+ .with_context(|| format!("Creating payloader {}", codec.payloader.name()))?;
+ let parse_filter = make_element("capsfilter", None)?;
+
+ pay.set_property("mtu", 1200 as u32);
+ pay.set_property("pt", codec.payload as u32);
+
+ if let Some(ssrc) = ssrc {
+ pay.set_property("ssrc", ssrc);
+ }
+
+ pipeline
+ .add_many(&[&conv, &conv_filter, &enc, &parse_filter, &pay])
+ .unwrap();
+ gst::Element::link_many(&[src, &conv, &conv_filter, &enc])
+ .with_context(|| "Linking encoding elements")?;
+
+ let codec_name = codec.caps.structure(0).unwrap().name();
+
+ if let Some(parser) = if codec_name == "video/x-h264" {
+ Some(make_element("h264parse", None)?)
+ } else if codec_name == "video/x-h265" {
+ Some(make_element("h265parse", None)?)
+ } else {
+ None
+ } {
+ pipeline.add(&parser).unwrap();
+ gst::Element::link_many(&[&enc, &parser, &parse_filter])
+ .with_context(|| "Linking encoding elements")?;
+ } else {
+ gst::Element::link_many(&[&enc, &parse_filter])
+ .with_context(|| "Linking encoding elements")?;
+ }
+
+ let conv_caps = if codec.is_video() {
+ let mut structure_builder = gst::Structure::builder("video/x-raw")
+ .field("pixel-aspect-ratio", gst::Fraction::new(1, 1));
+
+ if codec.encoder.name() == "nvh264enc" {
+ // Quirk: nvh264enc can perform conversion from RGB formats, but
+ // doesn't advertise / negotiate colorimetry correctly, leading
+ // to incorrect color display in Chrome (but interestingly not in
+ // Firefox). In any case, restrict to exclude RGB formats altogether,
+ // and let videoconvert do the conversion properly if needed.
+ structure_builder =
+ structure_builder.field("format", &gst::List::new(&[&"NV12", &"YV12", &"I420"]));
+ }
+
+ gst::Caps::builder_full_with_any_features()
+ .structure(structure_builder.build())
+ .build()
+ } else {
+ gst::Caps::builder("audio/x-raw").build()
+ };
+
+ match codec.encoder.name().as_str() {
+ "vp8enc" | "vp9enc" => {
+ pay.set_property_from_str("picture-id-mode", "15-bit");
+ }
+ _ => (),
+ }
+
+ /* We only enforce TWCC in the offer caps, once a remote description
+ * has been set it will get automatically negotiated. This is necessary
+ * because the implementor in Firefox had apparently not understood the
+ * concept of *transport-wide* congestion control, and firefox doesn't
+ * provide feedback for audio packets.
+ */
+ if twcc {
+ let twcc_extension = gst_rtp::RTPHeaderExtension::create_from_uri(RTP_TWCC_URI).unwrap();
+ twcc_extension.set_id(1);
+ pay.emit_by_name::<()>("add-extension", &[&twcc_extension]);
+ }
+
+ conv_filter.set_property("caps", conv_caps);
+
+ let parse_caps = if codec_name == "video/x-h264" {
+ gst::Caps::builder(codec_name)
+ .field("stream-format", "avc")
+ .field("profile", "constrained-baseline")
+ .build()
+ } else if codec_name == "video/x-h265" {
+ gst::Caps::builder(codec_name)
+ .field("stream-format", "hvc1")
+ .build()
+ } else {
+ gst::Caps::new_any()
+ };
+
+ parse_filter.set_property("caps", parse_caps);
+
+ gst::Element::link_many(&[&parse_filter, &pay]).with_context(|| "Linking encoding elements")?;
+
+ Ok((enc, conv_filter, pay))
+}
+
+impl VideoEncoder {
+ fn new(
+ element: gst::Element,
+ filter: gst::Element,
+ video_info: gst_video::VideoInfo,
+ peer_id: &str,
+ codec_name: &str,
+ transceiver: gst_webrtc::WebRTCRTPTransceiver,
+ ) -> Self {
+ let halved_framerate = video_info.fps().mul(gst::Fraction::new(1, 2));
+
+ Self {
+ factory_name: element.factory().unwrap().name().into(),
+ codec_name: codec_name.to_string(),
+ element,
+ filter,
+ halved_framerate,
+ video_info,
+ session_id: peer_id.to_string(),
+ mitigation_mode: WebRTCSinkMitigationMode::NONE,
+ transceiver,
+ }
+ }
+
+ pub fn bitrate(&self) -> i32 {
+ match self.factory_name.as_str() {
+ "vp8enc" | "vp9enc" => self.element.property::<i32>("target-bitrate"),
+ "x264enc" | "nvh264enc" | "vaapih264enc" | "vaapivp8enc" => {
+ (self.element.property::<u32>("bitrate") * 1000) as i32
+ }
+ "nvv4l2h264enc" | "nvv4l2vp8enc" => (self.element.property::<u32>("bitrate")) as i32,
+ factory => unimplemented!("Factory {} is currently not supported", factory),
+ }
+ }
+
+ pub fn scale_height_round_2(&self, height: i32) -> i32 {
+ let ratio = gst_video::calculate_display_ratio(
+ self.video_info.width(),
+ self.video_info.height(),
+ self.video_info.par(),
+ gst::Fraction::new(1, 1),
+ )
+ .unwrap();
+
+ let width = height.mul_div_ceil(ratio.numer(), ratio.denom()).unwrap();
+
+ (width + 1) & !1
+ }
+
+ pub fn set_bitrate(&mut self, element: &super::WebRTCSink, bitrate: i32) {
+ match self.factory_name.as_str() {
+ "vp8enc" | "vp9enc" => self.element.set_property("target-bitrate", bitrate),
+ "x264enc" | "nvh264enc" | "vaapih264enc" | "vaapivp8enc" => self
+ .element
+ .set_property("bitrate", (bitrate / 1000) as u32),
+ "nvv4l2h264enc" | "nvv4l2vp8enc" => {
+ self.element.set_property("bitrate", bitrate as u32)
+ }
+ factory => unimplemented!("Factory {} is currently not supported", factory),
+ }
+
+ let current_caps = self.filter.property::<gst::Caps>("caps");
+ let mut s = current_caps.structure(0).unwrap().to_owned();
+
+ // Hardcoded thresholds, may be tuned further in the future, and
+ // adapted according to the codec in use
+ if bitrate < 500000 {
+ let height = 360i32.min(self.video_info.height() as i32);
+ let width = self.scale_height_round_2(height);
+
+ s.set("height", height);
+ s.set("width", width);
+
+ if self.halved_framerate.numer() != 0 {
+ s.set("framerate", self.halved_framerate);
+ }
+
+ self.mitigation_mode =
+ WebRTCSinkMitigationMode::DOWNSAMPLED | WebRTCSinkMitigationMode::DOWNSCALED;
+ } else if bitrate < 1000000 {
+ let height = 360i32.min(self.video_info.height() as i32);
+ let width = self.scale_height_round_2(height);
+
+ s.set("height", height);
+ s.set("width", width);
+ s.remove_field("framerate");
+
+ self.mitigation_mode = WebRTCSinkMitigationMode::DOWNSCALED;
+ } else if bitrate < 2000000 {
+ let height = 720i32.min(self.video_info.height() as i32);
+ let width = self.scale_height_round_2(height);
+
+ s.set("height", height);
+ s.set("width", width);
+ s.remove_field("framerate");
+
+ self.mitigation_mode = WebRTCSinkMitigationMode::DOWNSCALED;
+ } else {
+ s.remove_field("height");
+ s.remove_field("width");
+ s.remove_field("framerate");
+
+ self.mitigation_mode = WebRTCSinkMitigationMode::NONE;
+ }
+
+ let caps = gst::Caps::builder_full_with_any_features()
+ .structure(s)
+ .build();
+
+ if !caps.is_strictly_equal(&current_caps) {
+ gst::log!(
+ CAT,
+ obj: element,
+ "session {}: setting bitrate {} and caps {} on encoder {:?}",
+ self.session_id,
+ bitrate,
+ caps,
+ self.element
+ );
+
+ self.filter.set_property("caps", caps);
+ }
+ }
+
+ fn gather_stats(&self) -> gst::Structure {
+ gst::Structure::builder("application/x-webrtcsink-video-encoder-stats")
+ .field("bitrate", self.bitrate())
+ .field("mitigation-mode", self.mitigation_mode)
+ .field("codec-name", self.codec_name.as_str())
+ .field(
+ "fec-percentage",
+ self.transceiver.property::<u32>("fec-percentage"),
+ )
+ .build()
+ }
+}
+
+impl State {
+ fn finalize_session(
+ &mut self,
+ element: &super::WebRTCSink,
+ session: &mut Session,
+ signal: bool,
+ ) {
+ gst::info!(CAT, "Ending session {}", session.id);
+ session.pipeline.debug_to_dot_file_with_ts(
+ gst::DebugGraphDetails::all(),
+ format!("removing-peer-{}-", session.peer_id,),
+ );
+
+ for ssrc in session.webrtc_pads.keys() {
+ session.links.remove(ssrc);
+ }
+
+ session.pipeline.call_async(|pipeline| {
+ let _ = pipeline.set_state(gst::State::Null);
+ });
+
+ if signal {
+ self.signaller.session_ended(element, &session.peer_id);
+ }
+ }
+
+ fn end_session(
+ &mut self,
+ element: &super::WebRTCSink,
+ session_id: &str,
+ signal: bool,
+ ) -> Option<Session> {
+ if let Some(mut session) = self.sessions.remove(session_id) {
+ self.finalize_session(element, &mut session, signal);
+ Some(session)
+ } else {
+ None
+ }
+ }
+
+ fn maybe_start_signaller(&mut self, element: &super::WebRTCSink) {
+ if self.signaller_state == SignallerState::Stopped
+ && element.current_state() >= gst::State::Paused
+ && self.codec_discovery_done
+ {
+ if let Err(err) = self.signaller.start(element) {
+ gst::error!(CAT, obj: element, "error: {}", err);
+ gst::element_error!(
+ element,
+ gst::StreamError::Failed,
+ ["Failed to start signaller {}", err]
+ );
+ } else {
+ gst::info!(CAT, "Started signaller");
+ self.signaller_state = SignallerState::Started;
+ }
+ }
+ }
+
+ fn maybe_stop_signaller(&mut self, element: &super::WebRTCSink) {
+ if self.signaller_state == SignallerState::Started {
+ self.signaller.stop(element);
+ self.signaller_state = SignallerState::Stopped;
+ gst::info!(CAT, "Stopped signaller");
+ }
+ }
+}
+
+impl Session {
+ fn new(
+ id: String,
+ pipeline: gst::Pipeline,
+ webrtcbin: gst::Element,
+ peer_id: String,
+ congestion_controller: Option<CongestionController>,
+ rtpgccbwe: Option<gst::Element>,
+ cc_info: CCInfo,
+ ) -> Self {
+ Self {
+ id,
+ pipeline,
+ webrtcbin,
+ peer_id,
+ cc_info,
+ rtprtxsend: None,
+ congestion_controller,
+ rtpgccbwe,
+ stats: gst::Structure::new_empty("application/x-webrtc-stats"),
+ sdp: None,
+ webrtc_pads: HashMap::new(),
+ encoders: Vec::new(),
+ links: HashMap::new(),
+ stats_sigid: None,
+ }
+ }
+
+ fn gather_stats(&self) -> gst::Structure {
+ let mut ret = self.stats.to_owned();
+
+ let encoder_stats: Vec<_> = self
+ .encoders
+ .iter()
+ .map(VideoEncoder::gather_stats)
+ .map(|s| s.to_send_value())
+ .collect();
+
+ let our_stats = gst::Structure::builder("application/x-webrtcsink-consumer-stats")
+ .field("video-encoders", gst::Array::from(encoder_stats))
+ .build();
+
+ ret.set("consumer-stats", our_stats);
+
+ ret
+ }
+
+ fn generate_ssrc(&self) -> u32 {
+ loop {
+ let ret = fastrand::u32(..);
+
+ if !self.webrtc_pads.contains_key(&ret) {
+ return ret;
+ }
+ }
+ }
+
+ /// Request a sink pad on our webrtcbin, and set its transceiver's codec_preferences
+ fn request_webrtcbin_pad(
+ &mut self,
+ element: &super::WebRTCSink,
+ settings: &Settings,
+ stream: &InputStream,
+ ) {
+ let ssrc = self.generate_ssrc();
+ let media_idx = self.webrtc_pads.len() as i32;
+
+ let mut payloader_caps = stream.out_caps.as_ref().unwrap().to_owned();
+
+ {
+ let payloader_caps_mut = payloader_caps.make_mut();
+ payloader_caps_mut.set_simple(&[("ssrc", &ssrc)]);
+ }
+
+ gst::info!(
+ CAT,
+ obj: element,
+ "Requesting WebRTC pad for consumer {} with caps {}",
+ self.peer_id,
+ payloader_caps
+ );
+
+ let pad = self
+ .webrtcbin
+ .request_pad_simple(&format!("sink_{}", media_idx))
+ .unwrap();
+
+ let transceiver = pad.property::<gst_webrtc::WebRTCRTPTransceiver>("transceiver");
+
+ transceiver.set_property(
+ "direction",
+ gst_webrtc::WebRTCRTPTransceiverDirection::Sendonly,
+ );
+
+ transceiver.set_property("codec-preferences", &payloader_caps);
+
+ if stream.sink_pad.name().starts_with("video_") {
+ if settings.do_fec {
+ transceiver.set_property("fec-type", gst_webrtc::WebRTCFECType::UlpRed);
+ }
+
+ transceiver.set_property("do-nack", settings.do_retransmission);
+ }
+
+ self.webrtc_pads.insert(
+ ssrc,
+ WebRTCPad {
+ pad,
+ in_caps: stream.in_caps.as_ref().unwrap().clone(),
+ media_idx: media_idx as u32,
+ ssrc,
+ stream_name: stream.sink_pad.name().to_string(),
+ payload: None,
+ },
+ );
+ }
+
+ /// Called when we have received an answer, connects an InputStream
+ /// to a given WebRTCPad
+ fn connect_input_stream(
+ &mut self,
+ element: &super::WebRTCSink,
+ producer: &StreamProducer,
+ webrtc_pad: &WebRTCPad,
+ codecs: &BTreeMap<i32, Codec>,
+ ) -> Result<(), Error> {
+ gst::info!(
+ CAT,
+ obj: element,
+ "Connecting input stream {} for consumer {}",
+ webrtc_pad.stream_name,
+ self.peer_id
+ );
+
+ let payload = webrtc_pad.payload.unwrap();
+
+ let codec = codecs
+ .get(&payload)
+ .ok_or_else(|| anyhow!("No codec for payload {}", payload))?;
+
+ let appsrc = make_element("appsrc", Some(&webrtc_pad.stream_name))?;
+ self.pipeline.add(&appsrc).unwrap();
+
+ let pay_filter = make_element("capsfilter", None)?;
+ self.pipeline.add(&pay_filter).unwrap();
+
+ let (enc, raw_filter, pay) = setup_encoding(
+ &self.pipeline,
+ &appsrc,
+ &webrtc_pad.in_caps,
+ codec,
+ Some(webrtc_pad.ssrc),
+ false,
+ )?;
+
+ element.emit_by_name::<bool>(
+ "encoder-setup",
+ &[&self.peer_id, &webrtc_pad.stream_name, &enc],
+ );
+
+ // At this point, the peer has provided its answer, and we want to
+ // let the payloader / encoder perform negotiation according to that.
+ //
+ // This means we need to unset our codec preferences, as they would now
+ // conflict with what the peer actually requested (see webrtcbin's
+ // caps query implementation), and instead install a capsfilter downstream
+ // of the payloader with caps constructed from the relevant SDP media.
+ let transceiver = webrtc_pad
+ .pad
+ .property::<gst_webrtc::WebRTCRTPTransceiver>("transceiver");
+ transceiver.set_property("codec-preferences", None::<gst::Caps>);
+
+ let mut global_caps = gst::Caps::new_simple("application/x-unknown", &[]);
+
+ let sdp = self.sdp.as_ref().unwrap();
+ let sdp_media = sdp.media(webrtc_pad.media_idx).unwrap();
+
+ sdp.attributes_to_caps(global_caps.get_mut().unwrap())
+ .unwrap();
+ sdp_media
+ .attributes_to_caps(global_caps.get_mut().unwrap())
+ .unwrap();
+
+ let caps = sdp_media
+ .caps_from_media(payload)
+ .unwrap()
+ .intersect(&global_caps);
+ let s = caps.structure(0).unwrap();
+ let mut filtered_s = gst::Structure::new_empty("application/x-rtp");
+
+ filtered_s.extend(s.iter().filter_map(|(key, value)| {
+ if key.starts_with("a-") {
+ None
+ } else {
+ Some((key, value.to_owned()))
+ }
+ }));
+ filtered_s.set("ssrc", webrtc_pad.ssrc);
+
+ let caps = gst::Caps::builder_full().structure(filtered_s).build();
+
+ pay_filter.set_property("caps", caps);
+
+ if codec.is_video() {
+ let video_info = gst_video::VideoInfo::from_caps(&webrtc_pad.in_caps)?;
+ let mut enc = VideoEncoder::new(
+ enc,
+ raw_filter,
+ video_info,
+ &self.peer_id,
+ codec.caps.structure(0).unwrap().name(),
+ transceiver,
+ );
+
+ match self.cc_info.heuristic {
+ WebRTCSinkCongestionControl::Disabled => {
+ // If congestion control is disabled, we simply use the highest
+ // known "safe" value for the bitrate.
+ enc.set_bitrate(element, self.cc_info.max_bitrate as i32);
+ enc.transceiver.set_property("fec-percentage", 50u32);
+ }
+ WebRTCSinkCongestionControl::Homegrown => {
+ if let Some(congestion_controller) = self.congestion_controller.as_mut() {
+ congestion_controller.target_bitrate_on_delay += enc.bitrate();
+ congestion_controller.target_bitrate_on_loss =
+ congestion_controller.target_bitrate_on_delay;
+ enc.transceiver.set_property("fec-percentage", 0u32);
+ } else {
+ /* If congestion control is disabled, we simply use the highest
+ * known "safe" value for the bitrate. */
+ enc.set_bitrate(element, self.cc_info.max_bitrate as i32);
+ enc.transceiver.set_property("fec-percentage", 50u32);
+ }
+ }
+ _ => enc.transceiver.set_property("fec-percentage", 0u32),
+ }
+
+ self.encoders.push(enc);
+
+ if let Some(ref rtpgccbwe) = self.rtpgccbwe.as_ref() {
+ let max_bitrate = self.cc_info.max_bitrate * (self.encoders.len() as u32);
+ rtpgccbwe.set_property("max-bitrate", max_bitrate);
+ }
+ }
+
+ let appsrc = appsrc.downcast::<gst_app::AppSrc>().unwrap();
+ gst_utils::StreamProducer::configure_consumer(&appsrc);
+ self.pipeline
+ .sync_children_states()
+ .with_context(|| format!("Connecting input stream for {}", self.peer_id))?;
+
+ pay.link(&pay_filter)?;
+
+ let srcpad = pay_filter.static_pad("src").unwrap();
+
+ srcpad
+ .link(&webrtc_pad.pad)
+ .with_context(|| format!("Connecting input stream for {}", self.peer_id))?;
+
+ match producer.add_consumer(&appsrc) {
+ Ok(link) => {
+ self.links.insert(webrtc_pad.ssrc, link);
+ Ok(())
+ }
+ Err(err) => Err(anyhow!("Could not link producer: {:?}", err)),
+ }
+ }
+}
+
+impl Drop for PipelineWrapper {
+ fn drop(&mut self) {
+ let _ = self.0.set_state(gst::State::Null);
+ }
+}
+
+impl InputStream {
+ /// Called when transitioning state up to Paused
+ fn prepare(&mut self, element: &super::WebRTCSink) -> Result<(), Error> {
+ let clocksync = make_element("clocksync", None)?;
+ let appsink = make_element("appsink", None)?
+ .downcast::<gst_app::AppSink>()
+ .unwrap();
+
+ element.add(&clocksync).unwrap();
+ element.add(&appsink).unwrap();
+
+ clocksync
+ .link(&appsink)
+ .with_context(|| format!("Linking input stream {}", self.sink_pad.name()))?;
+
+ element
+ .sync_children_states()
+ .with_context(|| format!("Linking input stream {}", self.sink_pad.name()))?;
+
+ self.sink_pad
+ .set_target(Some(&clocksync.static_pad("sink").unwrap()))
+ .unwrap();
+
+ let producer = StreamProducer::from(&appsink);
+ producer.forward();
+
+ self.producer = Some(producer);
+
+ Ok(())
+ }
+
+ /// Called when transitioning state back down to Ready
+ fn unprepare(&mut self, element: &super::WebRTCSink) {
+ self.sink_pad.set_target(None::<&gst::Pad>).unwrap();
+
+ if let Some(clocksync) = self.clocksync.take() {
+ element.remove(&clocksync).unwrap();
+ clocksync.set_state(gst::State::Null).unwrap();
+ }
+
+ if let Some(producer) = self.producer.take() {
+ let appsink = producer.appsink().upcast_ref::<gst::Element>();
+ element.remove(appsink).unwrap();
+ appsink.set_state(gst::State::Null).unwrap();
+ }
+ }
+}
+
+impl NavigationEventHandler {
+ pub fn new(element: &super::WebRTCSink, webrtcbin: &gst::Element) -> Self {
+ gst::info!(CAT, "Creating navigation data channel");
+ let channel = webrtcbin.emit_by_name::<WebRTCDataChannel>(
+ "create-data-channel",
+ &[
+ &"input",
+ &gst::Structure::new(
+ "config",
+ &[("priority", &gst_webrtc::WebRTCPriorityType::High)],
+ ),
+ ],
+ );
+
+ let weak_element = element.downgrade();
+ Self((
+ channel.connect("on-message-string", false, move |values| {
+ if let Some(element) = weak_element.upgrade() {
+ let _channel = values[0].get::<WebRTCDataChannel>().unwrap();
+ let msg = values[1].get::<&str>().unwrap();
+ create_navigation_event(&element, msg);
+ }
+
+ None
+ }),
+ channel,
+ ))
+ }
+}
+
+impl WebRTCSink {
+ /// Build an ordered map of Codecs, given user-provided audio / video caps */
+ fn lookup_codecs(&self) -> BTreeMap<i32, Codec> {
+ /* First gather all encoder and payloader factories */
+ let encoders = gst::ElementFactory::factories_with_type(
+ gst::ElementFactoryType::ENCODER,
+ gst::Rank::Marginal,
+ );
+
+ let payloaders = gst::ElementFactory::factories_with_type(
+ gst::ElementFactoryType::PAYLOADER,
+ gst::Rank::Marginal,
+ );
+
+ /* Now iterate user-provided codec preferences and determine
+ * whether we can fulfill these preferences */
+ let settings = self.settings.lock().unwrap();
+ let mut payload = 96..128;
+
+ settings
+ .video_caps
+ .iter()
+ .chain(settings.audio_caps.iter())
+ .filter_map(move |s| {
+ let caps = gst::Caps::builder_full().structure(s.to_owned()).build();
+
+ Option::zip(
+ encoders
+ .iter()
+ .find(|factory| factory.can_src_any_caps(&caps)),
+ payloaders
+ .iter()
+ .find(|factory| factory.can_sink_any_caps(&caps)),
+ )
+ .and_then(|(encoder, payloader)| {
+ /* Assign a payload type to the codec */
+ if let Some(pt) = payload.next() {
+ Some(Codec {
+ encoder: encoder.clone(),
+ payloader: payloader.clone(),
+ caps,
+ payload: pt,
+ })
+ } else {
+ gst::warning!(CAT, imp: self,
+ "Too many formats for available payload type range, ignoring {}",
+ s);
+ None
+ }
+ })
+ })
+ .map(|codec| (codec.payload, codec))
+ .collect()
+ }
+
+ /// Prepare for accepting consumers, by setting
+ /// up StreamProducers for each of our sink pads
+ fn prepare(&self, element: &super::WebRTCSink) -> Result<(), Error> {
+ gst::debug!(CAT, obj: element, "preparing");
+
+ self.state
+ .lock()
+ .unwrap()
+ .streams
+ .iter_mut()
+ .try_for_each(|(_, stream)| stream.prepare(element))?;
+
+ Ok(())
+ }
+
+ /// Unprepare by stopping consumers, then the signaller object.
+ /// Might abort codec discovery
+ fn unprepare(&self, element: &super::WebRTCSink) -> Result<(), Error> {
+ gst::info!(CAT, obj: element, "unpreparing");
+
+ let mut state = self.state.lock().unwrap();
+
+ let session_ids: Vec<_> = state.sessions.keys().map(|k| k.to_owned()).collect();
+
+ for id in session_ids {
+ state.end_session(element, &id, true);
+ }
+
+ state
+ .streams
+ .iter_mut()
+ .for_each(|(_, stream)| stream.unprepare(element));
+
+ if let Some(handle) = state.codecs_abort_handle.take() {
+ handle.abort();
+ }
+
+ if let Some(receiver) = state.codecs_done_receiver.take() {
+ task::block_on(async {
+ let _ = receiver.await;
+ });
+ }
+
+ state.maybe_stop_signaller(element);
+
+ state.codec_discovery_done = false;
+ state.codecs = BTreeMap::new();
+
+ Ok(())
+ }
+
+ /// When using a custom signaller
+ pub fn set_signaller(&self, signaller: Box<dyn super::SignallableObject>) -> Result<(), Error> {
+ let mut state = self.state.lock().unwrap();
+
+ state.signaller = signaller;
+
+ Ok(())
+ }
+
+ /// Called by the signaller when it has encountered an error
+ pub fn handle_signalling_error(&self, element: &super::WebRTCSink, error: anyhow::Error) {
+ gst::error!(CAT, obj: element, "Signalling error: {:?}", error);
+
+ gst::element_error!(
+ element,
+ gst::StreamError::Failed,
+ ["Signalling error: {:?}", error]
+ );
+ }
+
+ fn on_offer_created(
+ &self,
+ element: &super::WebRTCSink,
+ offer: gst_webrtc::WebRTCSessionDescription,
+ session_id: &str,
+ ) {
+ let mut state = self.state.lock().unwrap();
+
+ if let Some(session) = state.sessions.get(session_id) {
+ session
+ .webrtcbin
+ .emit_by_name::<()>("set-local-description", &[&offer, &None::<gst::Promise>]);
+
+ if let Err(err) = state.signaller.handle_sdp(element, session_id, &offer) {
+ gst::warning!(
+ CAT,
+ "Failed to handle SDP for session {}: {}",
+ session_id,
+ err
+ );
+
+ state.end_session(element, session_id, true);
+ }
+ }
+ }
+
+ fn negotiate(&self, element: &super::WebRTCSink, session_id: &str) {
+ let state = self.state.lock().unwrap();
+
+ gst::debug!(CAT, obj: element, "Negotiating for session {}", session_id);
+
+ if let Some(session) = state.sessions.get(session_id) {
+ let element = element.downgrade();
+ gst::debug!(CAT, "Creating offer for session {}", session_id);
+ let session_id = session_id.to_string();
+ let promise = gst::Promise::with_change_func(move |reply| {
+ gst::debug!(CAT, "Created offer for session {}", session_id);
+
+ if let Some(element) = element.upgrade() {
+ let this = Self::from_instance(&element);
+ let reply = match reply {
+ Ok(Some(reply)) => reply,
+ Ok(None) => {
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Promise returned without a reply for {}",
+ session_id
+ );
+ let _ = this.remove_session(&element, &session_id, true);
+ return;
+ }
+ Err(err) => {
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Promise returned with an error for {}: {:?}",
+ session_id,
+ err
+ );
+ let _ = this.remove_session(&element, &session_id, true);
+ return;
+ }
+ };
+
+ if let Ok(offer) = reply
+ .value("offer")
+ .map(|offer| offer.get::<gst_webrtc::WebRTCSessionDescription>().unwrap())
+ {
+ this.on_offer_created(&element, offer, &session_id);
+ } else {
+ gst::warning!(
+ CAT,
+ "Reply without an offer for session {}: {:?}",
+ session_id,
+ reply
+ );
+ let _ = this.remove_session(&element, &session_id, true);
+ }
+ }
+ });
+
+ session
+ .webrtcbin
+ .emit_by_name::<()>("create-offer", &[&None::<gst::Structure>, &promise]);
+ } else {
+ gst::debug!(
+ CAT,
+ obj: element,
+ "consumer for session {} no longer exists (sessions: {:?}",
+ session_id,
+ state.sessions.keys().map(|id| id)
+ );
+ }
+ }
+
+ fn on_ice_candidate(
+ &self,
+ element: &super::WebRTCSink,
+ session_id: String,
+ sdp_m_line_index: u32,
+ candidate: String,
+ ) {
+ let mut state = self.state.lock().unwrap();
+ if let Err(err) = state.signaller.handle_ice(
+ element,
+ &session_id,
+ &candidate,
+ Some(sdp_m_line_index),
+ None,
+ ) {
+ gst::warning!(
+ CAT,
+ "Failed to handle ICE in session {}: {}",
+ session_id,
+ err
+ );
+
+ state.end_session(element, &session_id, true);
+ }
+ }
+
+ /// Called by the signaller to add a new session
+ pub fn start_session(
+ &self,
+ element: &super::WebRTCSink,
+ session_id: &str,
+ peer_id: &str,
+ ) -> Result<(), WebRTCSinkError> {
+ let settings = self.settings.lock().unwrap();
+ let mut state = self.state.lock().unwrap();
+ let peer_id = peer_id.to_string();
+ let session_id = session_id.to_string();
+
+ if state.sessions.contains_key(&session_id) {
+ return Err(WebRTCSinkError::DuplicateSessionId(session_id));
+ }
+
+ gst::info!(
+ CAT,
+ obj: element,
+ "Adding session: {} for peer: {}",
+ peer_id,
+ session_id
+ );
+
+ let pipeline = gst::Pipeline::new(Some(&format!("session-pipeline-{}", session_id)));
+
+ let webrtcbin = make_element("webrtcbin", Some(&format!("webrtcbin-{}", session_id)))
+ .map_err(|err| WebRTCSinkError::SessionPipelineError {
+ session_id: session_id.clone(),
+ peer_id: peer_id.clone(),
+ details: err.to_string(),
+ })?;
+
+ webrtcbin.set_property_from_str("bundle-policy", "max-bundle");
+
+ if let Some(stun_server) = settings.stun_server.as_ref() {
+ webrtcbin.set_property("stun-server", stun_server);
+ }
+
+ if let Some(turn_server) = settings.turn_server.as_ref() {
+ webrtcbin.set_property("turn-server", turn_server);
+ }
+
+ let rtpgccbwe = match settings.cc_info.heuristic {
+ WebRTCSinkCongestionControl::GoogleCongestionControl => {
+ let rtpgccbwe = match gst::ElementFactory::make("rtpgccbwe", None) {
+ Err(err) => {
+ glib::g_warning!(
+ "webrtcsink",
+ "The `rtpgccbwe` element is not available \
+ not doing any congestion control: {err:?}"
+ );
+ None
+ }
+ Ok(cc) => {
+ webrtcbin.connect_closure(
+ "request-aux-sender",
+ false,
+ glib::closure!(@watch element, @strong session_id, @weak-allow-none cc
+ => move |_webrtcbin: gst::Element, _transport: gst::Object| {
+
+ let cc = cc.unwrap();
+ let settings = element.imp().settings.lock().unwrap();
+
+ // TODO: Bind properties with @element's
+ cc.set_properties(&[
+ ("min-bitrate", &settings.cc_info.min_bitrate),
+ ("estimated-bitrate", &settings.cc_info.start_bitrate),
+ ("max-bitrate", &settings.cc_info.max_bitrate),
+ ]);
+
+ cc.connect_notify(Some("estimated-bitrate"),
+ glib::clone!(@weak element, @strong session_id
+ => move |bwe, pspec| {
+ element.imp().set_bitrate(&element, &session_id,
+ bwe.property::<u32>(pspec.name()));
+ }
+ ));
+
+ Some(cc)
+ }),
+ );
+
+ Some(cc)
+ }
+ };
+
+ webrtcbin.connect_closure(
+ "deep-element-added",
+ false,
+ glib::closure!(@watch element, @strong session_id
+ => move |_webrtcbin: gst::Element, _bin: gst::Bin, e: gst::Element| {
+
+ if e.factory().map_or(false, |f| f.name() == "rtprtxsend") {
+ if e.has_property("stuffing-kbps", Some(i32::static_type())) {
+ element.imp().set_rtptrxsend(&element, &session_id, e);
+ } else {
+ gst::warning!(CAT, "rtprtxsend doesn't have a `stuffing-kbps` \
+ property, stuffing disabled");
+ }
+ }
+ }),
+ );
+
+ rtpgccbwe
+ }
+ _ => None,
+ };
+
+ pipeline.add(&webrtcbin).unwrap();
+
+ let element_clone = element.downgrade();
+ let session_id_clone = session_id.clone();
+ webrtcbin.connect("on-ice-candidate", false, move |values| {
+ if let Some(element) = element_clone.upgrade() {
+ let this = Self::from_instance(&element);
+ let sdp_m_line_index = values[1].get::<u32>().expect("Invalid argument");
+ let candidate = values[2].get::<String>().expect("Invalid argument");
+ this.on_ice_candidate(
+ &element,
+ session_id_clone.to_string(),
+ sdp_m_line_index,
+ candidate,
+ );
+ }
+ None
+ });
+
+ let element_clone = element.downgrade();
+ let peer_id_clone = peer_id.clone();
+ let session_id_clone = session_id.clone();
+ webrtcbin.connect_notify(Some("connection-state"), move |webrtcbin, _pspec| {
+ if let Some(element) = element_clone.upgrade() {
+ let state =
+ webrtcbin.property::<gst_webrtc::WebRTCPeerConnectionState>("connection-state");
+
+ match state {
+ gst_webrtc::WebRTCPeerConnectionState::Failed => {
+ let this = Self::from_instance(&element);
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Connection state for in session {} (peer {}) failed",
+ session_id_clone,
+ peer_id_clone
+ );
+ let _ = this.remove_session(&element, &session_id_clone, true);
+ }
+ _ => {
+ gst::log!(
+ CAT,
+ obj: &element,
+ "Connection state in session {} (peer {}) changed: {:?}",
+ session_id_clone,
+ peer_id_clone,
+ state
+ );
+ }
+ }
+ }
+ });
+
+ let element_clone = element.downgrade();
+ let peer_id_clone = peer_id.clone();
+ let session_id_clone = session_id.clone();
+ webrtcbin.connect_notify(Some("ice-connection-state"), move |webrtcbin, _pspec| {
+ if let Some(element) = element_clone.upgrade() {
+ let state = webrtcbin
+ .property::<gst_webrtc::WebRTCICEConnectionState>("ice-connection-state");
+ let this = Self::from_instance(&element);
+
+ match state {
+ gst_webrtc::WebRTCICEConnectionState::Failed => {
+ gst::warning!(
+ CAT,
+ obj: &element,
+ "Ice connection state in session {} (peer {}) failed",
+ session_id_clone,
+ peer_id_clone,
+ );
+ let _ = this.remove_session(&element, &session_id_clone, true);
+ }
+ _ => {
+ gst::log!(
+ CAT,
+ obj: &element,
+ "Ice connection state in session {} (peer {}) changed: {:?}",
+ session_id_clone,
+ peer_id_clone,
+ state
+ );
+ }
+ }
+
+ if state == gst_webrtc::WebRTCICEConnectionState::Completed {
+ let state = this.state.lock().unwrap();
+
+ if let Some(session) = state.sessions.get(&session_id_clone) {
+ for webrtc_pad in session.webrtc_pads.values() {
+ if let Some(srcpad) = webrtc_pad.pad.peer() {
+ srcpad.send_event(
+ gst_video::UpstreamForceKeyUnitEvent::builder()
+ .all_headers(true)
+ .build(),
+ );
+ }
+ }
+ }
+ }
+ }
+ });
+
+ let element_clone = element.downgrade();
+ let peer_id_clone = peer_id.clone();
+ let session_id_clone = session_id.clone();
+ webrtcbin.connect_notify(Some("ice-gathering-state"), move |webrtcbin, _pspec| {
+ let state =
+ webrtcbin.property::<gst_webrtc::WebRTCICEGatheringState>("ice-gathering-state");
+
+ if let Some(element) = element_clone.upgrade() {
+ gst::log!(
+ CAT,
+ obj: &element,
+ "Ice gathering state in session {} (peer {}) changed: {:?}",
+ session_id_clone,
+ peer_id_clone,
+ state
+ );
+ }
+ });
+
+ let mut session = Session::new(
+ session_id.clone(),
+ pipeline.clone(),
+ webrtcbin.clone(),
+ peer_id.clone(),
+ match settings.cc_info.heuristic {
+ WebRTCSinkCongestionControl::Homegrown => Some(CongestionController::new(
+ &peer_id,
+ settings.cc_info.min_bitrate,
+ settings.cc_info.max_bitrate,
+ )),
+ _ => None,
+ },
+ rtpgccbwe,
+ settings.cc_info,
+ );
+
+ let rtpbin = webrtcbin
+ .dynamic_cast_ref::<gst::ChildProxy>()
+ .unwrap()
+ .child_by_name("rtpbin")
+ .unwrap();
+
+ if session.congestion_controller.is_some() {
+ let session_id_str = session_id.to_string();
+ if session.stats_sigid.is_none() {
+ session.stats_sigid = Some(rtpbin.connect_closure("on-new-ssrc", true,
+ glib::closure!(@weak-allow-none element, @weak-allow-none webrtcbin
+ => move |rtpbin: gst::Object, session_id: u32, _src: u32| {
+ let rtp_session = rtpbin.emit_by_name::<gst::Element>("get-session", &[&session_id]);
+
+ let element = element.expect("on-new-ssrc emited when webrtcsink has been disposed?");
+ let webrtcbin = webrtcbin.unwrap();
+ let mut state = element.imp().state.lock().unwrap();
+ if let Some(mut session) = state.sessions.get_mut(&session_id_str) {
+
+ session.stats_sigid = Some(rtp_session.connect_notify(Some("twcc-stats"),
+ glib::clone!(@strong session_id_str, @weak webrtcbin, @weak element => @default-panic, move |sess, pspec| {
+ // Run the Loss-based control algorithm on new peer TWCC feedbacks
+ element.imp().process_loss_stats(&element, &session_id_str, &sess.property::<gst::Structure>(pspec.name()));
+ })
+ ));
+ }
+ })
+ ));
+ }
+ }
+
+ state
+ .streams
+ .iter()
+ .for_each(|(_, stream)| session.request_webrtcbin_pad(element, &settings, stream));
+
+ let clock = element.clock();
+
+ pipeline.use_clock(clock.as_ref());
+ pipeline.set_start_time(gst::ClockTime::NONE);
+ pipeline.set_base_time(element.base_time().unwrap());
+
+ let mut bus_stream = pipeline.bus().unwrap().stream();
+ let element_clone = element.downgrade();
+ let pipeline_clone = pipeline.downgrade();
+ let session_id_clone = session_id.to_owned();
+
+ task::spawn(async move {
+ while let Some(msg) = bus_stream.next().await {
+ if let Some(element) = element_clone.upgrade() {
+ let this = Self::from_instance(&element);
+ match msg.view() {
+ gst::MessageView::Error(err) => {
+ gst::error!(
+ CAT,
+ "session {} error: {}, details: {:?}",
+ session_id_clone,
+ err.error(),
+ err.debug()
+ );
+ let _ = this.remove_session(&element, &session_id_clone, true);
+ }
+ gst::MessageView::StateChanged(state_changed) => {
+ if let Some(pipeline) = pipeline_clone.upgrade() {
+ if Some(pipeline.clone().upcast()) == state_changed.src() {
+ pipeline.debug_to_dot_file_with_ts(
+ gst::DebugGraphDetails::all(),
+ format!(
+ "webrtcsink-session-{}-{:?}-to-{:?}",
+ session_id_clone,
+ state_changed.old(),
+ state_changed.current()
+ ),
+ );
+ }
+ }
+ }
+ gst::MessageView::Latency(..) => {
+ if let Some(pipeline) = pipeline_clone.upgrade() {
+ gst::info!(CAT, obj: &pipeline, "Recalculating latency");
+ let _ = pipeline.recalculate_latency();
+ }
+ }
+ gst::MessageView::Eos(..) => {
+ gst::error!(
+ CAT,
+ "Unexpected end of stream in session {}",
+ session_id_clone,
+ );
+ let _ = this.remove_session(&element, &session_id_clone, true);
+ }
+ _ => (),
+ }
+ }
+ }
+ });
+
+ pipeline.set_state(gst::State::Ready).map_err(|err| {
+ WebRTCSinkError::SessionPipelineError {
+ session_id: session_id.to_string(),
+ peer_id: peer_id.to_string(),
+ details: err.to_string(),
+ }
+ })?;
+
+ if settings.enable_data_channel_navigation {
+ state.navigation_handler = Some(NavigationEventHandler::new(element, &webrtcbin));
+ }
+
+ state.sessions.insert(session_id.to_string(), session);
+
+ drop(state);
+ drop(settings);
+
+ // This is intentionally emitted with the pipeline in the Ready state,
+ // so that application code can create data channels at the correct
+ // moment.
+ element.emit_by_name::<()>("consumer-added", &[&peer_id, &webrtcbin]);
+
+ // We don't connect to on-negotiation-needed, this in order to call the above
+ // signal without holding the state lock:
+ //
+ // Going to Ready triggers synchronous emission of the on-negotiation-needed
+ // signal, during which time the application may add a data channel, causing
+ // renegotiation, which we do not support at this time.
+ //
+ // This is completely safe, as we know that by now all conditions are gathered:
+ // webrtcbin is in the Ready state, and all its transceivers have codec_preferences.
+ self.negotiate(element, &session_id);
+
+ pipeline.set_state(gst::State::Playing).map_err(|err| {
+ WebRTCSinkError::SessionPipelineError {
+ session_id: session_id.to_string(),
+ peer_id: peer_id.to_string(),
+ details: err.to_string(),
+ }
+ })?;
+
+ Ok(())
+ }
+
+ /// Called by the signaller to remove a consumer
+ pub fn remove_session(
+ &self,
+ element: &super::WebRTCSink,
+ session_id: &str,
+ signal: bool,
+ ) -> Result<(), WebRTCSinkError> {
+ let mut state = self.state.lock().unwrap();
+
+ if !state.sessions.contains_key(session_id) {
+ return Err(WebRTCSinkError::NoSessionWithId(session_id.to_string()));
+ }
+
+ if let Some(session) = state.end_session(element, session_id, signal) {
+ drop(state);
+ element.emit_by_name::<()>("consumer-removed", &[&session.peer_id, &session.webrtcbin]);
+ }
+
+ Ok(())
+ }
+
+ fn process_loss_stats(
+ &self,
+ element: &super::WebRTCSink,
+ session_id: &str,
+ stats: &gst::Structure,
+ ) {
+ let mut state = element.imp().state.lock().unwrap();
+ if let Some(mut session) = state.sessions.get_mut(session_id) {
+ if let Some(congestion_controller) = session.congestion_controller.as_mut() {
+ congestion_controller.loss_control(&element, stats, &mut session.encoders);
+ }
+ session.stats = stats.to_owned();
+ }
+ }
+
+ fn process_stats(
+ &self,
+ element: &super::WebRTCSink,
+ webrtcbin: gst::Element,
+ session_id: &str,
+ ) {
+ let session_id = session_id.to_string();
+ let promise = gst::Promise::with_change_func(
+ glib::clone!(@strong session_id, @weak element => move |reply| {
+ if let Ok(Some(stats)) = reply {
+
+ let mut state = element.imp().state.lock().unwrap();
+ if let Some(mut session) = state.sessions.get_mut(&session_id) {
+ if let Some(congestion_controller) = session.congestion_controller.as_mut() {
+ congestion_controller.delay_control(&element, stats, &mut session.encoders,);
+ }
+ session.stats = stats.to_owned();
+ }
+ }
+ }),
+ );
+
+ webrtcbin.emit_by_name::<()>("get-stats", &[&None::<gst::Pad>, &promise]);
+ }
+
+ fn set_rtptrxsend(&self, element: &super::WebRTCSink, peer_id: &str, rtprtxsend: gst::Element) {
+ let mut state = element.imp().state.lock().unwrap();
+
+ if let Some(session) = state.sessions.get_mut(peer_id) {
+ session.rtprtxsend = Some(rtprtxsend);
+ }
+ }
+
+ fn set_bitrate(&self, element: &super::WebRTCSink, peer_id: &str, bitrate: u32) {
+ let settings = element.imp().settings.lock().unwrap();
+ let mut state = element.imp().state.lock().unwrap();
+
+ if let Some(session) = state.sessions.get_mut(peer_id) {
+ let fec_ratio = {
+ if settings.do_fec && bitrate > DO_FEC_THRESHOLD {
+ (bitrate as f64 - DO_FEC_THRESHOLD as f64)
+ / (session.cc_info.max_bitrate as f64 - DO_FEC_THRESHOLD as f64)
+ } else {
+ 0f64
+ }
+ };
+
+ let fec_percentage = fec_ratio * 50f64;
+ let encoders_bitrate = ((bitrate as f64)
+ / (1. + (fec_percentage / 100.))
+ / (session.encoders.len() as f64)) as i32;
+
+ if let Some(ref rtpxsend) = session.rtprtxsend.as_ref() {
+ rtpxsend.set_property("stuffing-kbps", (bitrate as f64 / 1000.) as i32);
+ }
+
+ for encoder in session.encoders.iter_mut() {
+ encoder.set_bitrate(element, encoders_bitrate);
+ encoder
+ .transceiver
+ .set_property("fec-percentage", fec_percentage as u32);
+ }
+ }
+ }
+
+ fn on_remote_description_set(&self, element: &super::WebRTCSink, session_id: String) {
+ let mut state = self.state.lock().unwrap();
+ let mut remove = false;
+ let codecs = state.codecs.clone();
+
+ if let Some(mut session) = state.sessions.remove(&session_id) {
+ for webrtc_pad in session.webrtc_pads.clone().values() {
+ let transceiver = webrtc_pad
+ .pad
+ .property::<gst_webrtc::WebRTCRTPTransceiver>("transceiver");
+
+ if let Some(mid) = transceiver.mid() {
+ state
+ .mids
+ .insert(mid.to_string(), webrtc_pad.stream_name.clone());
+ }
+
+ if let Some(producer) = state
+ .streams
+ .get(&webrtc_pad.stream_name)
+ .and_then(|stream| stream.producer.clone())
+ {
+ drop(state);
+ if let Err(err) =
+ session.connect_input_stream(element, &producer, webrtc_pad, &codecs)
+ {
+ gst::error!(
+ CAT,
+ obj: element,
+ "Failed to connect input stream {} for session {}: {}",
+ webrtc_pad.stream_name,
+ session_id,
+ err
+ );
+ remove = true;
+ state = self.state.lock().unwrap();
+ break;
+ }
+ state = self.state.lock().unwrap();
+ } else {
+ gst::error!(
+ CAT,
+ obj: element,
+ "No producer to connect session {} to",
+ session_id,
+ );
+ remove = true;
+ break;
+ }
+ }
+
+ session.pipeline.debug_to_dot_file_with_ts(
+ gst::DebugGraphDetails::all(),
+ format!("webrtcsink-peer-{}-remote-description-set", session_id,),
+ );
+
+ let element_clone = element.downgrade();
+ let webrtcbin = session.webrtcbin.downgrade();
+ task::spawn(async move {
+ let mut interval =
+ async_std::stream::interval(std::time::Duration::from_millis(100));
+
+ while interval.next().await.is_some() {
+ let element_clone = element_clone.clone();
+ if let (Some(webrtcbin), Some(element)) =
+ (webrtcbin.upgrade(), element_clone.upgrade())
+ {
+ element
+ .imp()
+ .process_stats(&element, webrtcbin, &session_id);
+ } else {
+ break;
+ }
+ }
+ });
+
+ if remove {
+ state.finalize_session(element, &mut session, true);
+ } else {
+ state.sessions.insert(session.id.clone(), session);
+ }
+ }
+ }
+
+ /// Called by the signaller with an ice candidate
+ pub fn handle_ice(
+ &self,
+ _element: &super::WebRTCSink,
+ session_id: &str,
+ sdp_m_line_index: Option<u32>,
+ _sdp_mid: Option<String>,
+ candidate: &str,
+ ) -> Result<(), WebRTCSinkError> {
+ let state = self.state.lock().unwrap();
+
+ let sdp_m_line_index = sdp_m_line_index.ok_or(WebRTCSinkError::MandatorySdpMlineIndex)?;
+
+ if let Some(session) = state.sessions.get(session_id) {
+ gst::trace!(CAT, "adding ice candidate for session {}", session_id);
+ session
+ .webrtcbin
+ .emit_by_name::<()>("add-ice-candidate", &[&sdp_m_line_index, &candidate]);
+ Ok(())
+ } else {
+ Err(WebRTCSinkError::NoSessionWithId(session_id.to_string()))
+ }
+ }
+
+ /// Called by the signaller with an answer to our offer
+ pub fn handle_sdp(
+ &self,
+ element: &super::WebRTCSink,
+ session_id: &str,
+ desc: &gst_webrtc::WebRTCSessionDescription,
+ ) -> Result<(), WebRTCSinkError> {
+ let mut state = self.state.lock().unwrap();
+
+ if let Some(session) = state.sessions.get_mut(session_id) {
+ let sdp = desc.sdp();
+
+ session.sdp = Some(sdp.to_owned());
+
+ for webrtc_pad in session.webrtc_pads.values_mut() {
+ let media_idx = webrtc_pad.media_idx;
+ /* TODO: support partial answer, webrtcbin doesn't seem
+ * very well equipped to deal with this at the moment */
+ if let Some(media) = sdp.media(media_idx) {
+ if media.attribute_val("inactive").is_some() {
+ let media_str = sdp
+ .media(webrtc_pad.media_idx)
+ .and_then(|media| media.as_text().ok());
+
+ gst::warning!(
+ CAT,
+ "consumer from session {} refused media {}: {:?}",
+ session_id,
+ media_idx,
+ media_str
+ );
+ state.end_session(element, session_id, true);
+
+ return Err(WebRTCSinkError::ConsumerRefusedMedia {
+ session_id: session_id.to_string(),
+ media_idx,
+ });
+ }
+ }
+
+ if let Some(payload) = sdp
+ .media(webrtc_pad.media_idx)
+ .and_then(|media| media.format(0))
+ .and_then(|format| format.parse::<i32>().ok())
+ {
+ webrtc_pad.payload = Some(payload);
+ } else {
+ gst::warning!(
+ CAT,
+ "consumer from session {} did not provide valid payload for media index {} for session {}",
+ session_id,
+ media_idx,
+ session_id,
+ );
+
+ state.end_session(element, session_id, true);
+
+ return Err(WebRTCSinkError::ConsumerNoValidPayload {
+ session_id: session_id.to_string(),
+ media_idx,
+ });
+ }
+ }
+
+ let element = element.downgrade();
+ let session_id = session_id.to_string();
+
+ let promise = gst::Promise::with_change_func(move |reply| {
+ gst::debug!(CAT, "received reply {:?}", reply);
+ if let Some(element) = element.upgrade() {
+ let this = Self::from_instance(&element);
+
+ this.on_remote_description_set(&element, session_id);
+ }
+ });
+
+ session
+ .webrtcbin
+ .emit_by_name::<()>("set-remote-description", &[desc, &promise]);
+
+ Ok(())
+ } else {
+ Err(WebRTCSinkError::NoSessionWithId(session_id.to_string()))
+ }
+ }
+
+ async fn run_discovery_pipeline(
+ _element: &super::WebRTCSink,
+ codec: &Codec,
+ caps: &gst::Caps,
+ ) -> Result<gst::Structure, Error> {
+ let pipe = PipelineWrapper(gst::Pipeline::new(None));
+
+ let src = if codec.is_video() {
+ make_element("videotestsrc", None)?
+ } else {
+ make_element("audiotestsrc", None)?
+ };
+ let mut elements = vec![src.clone()];
+
+ if codec.is_video() {
+ elements.push(make_converter_for_video_caps(caps)?);
+ }
+
+ let capsfilter = make_element("capsfilter", None)?;
+ elements.push(capsfilter.clone());
+ let elements_slice = &elements.iter().collect::<Vec<_>>();
+ pipe.0.add_many(elements_slice).unwrap();
+ gst::Element::link_many(elements_slice)
+ .with_context(|| format!("Running discovery pipeline for caps {}", caps))?;
+
+ let (_, _, pay) = setup_encoding(&pipe.0, &capsfilter, caps, codec, None, true)?;
+
+ let sink = make_element("fakesink", None)?;
+
+ pipe.0.add(&sink).unwrap();
+
+ pay.link(&sink)
+ .with_context(|| format!("Running discovery pipeline for caps {}", caps))?;
+
+ capsfilter.set_property("caps", caps);
+
+ src.set_property("num-buffers", 1);
+
+ let mut stream = pipe.0.bus().unwrap().stream();
+
+ pipe.0
+ .set_state(gst::State::Playing)
+ .with_context(|| format!("Running discovery pipeline for caps {}", caps))?;
+
+ while let Some(msg) = stream.next().await {
+ match msg.view() {
+ gst::MessageView::Error(err) => {
+ pipe.0.debug_to_dot_file_with_ts(
+ gst::DebugGraphDetails::all(),
+ "webrtcsink-discovery-error",
+ );
+ return Err(err.error().into());
+ }
+ gst::MessageView::Eos(_) => {
+ let caps = pay.static_pad("src").unwrap().current_caps().unwrap();
+
+ pipe.0.debug_to_dot_file_with_ts(
+ gst::DebugGraphDetails::all(),
+ "webrtcsink-discovery-done",
+ );
+
+ if let Some(s) = caps.structure(0) {
+ let mut s = s.to_owned();
+ s.remove_fields(&[
+ "timestamp-offset",
+ "seqnum-offset",
+ "ssrc",
+ "sprop-parameter-sets",
+ "a-framerate",
+ ]);
+ s.set("payload", codec.payload);
+ return Ok(s);
+ } else {
+ return Err(anyhow!("Discovered empty caps"));
+ }
+ }
+ _ => {
+ continue;
+ }
+ }
+ }
+
+ unreachable!()
+ }
+
+ async fn lookup_caps(
+ element: &super::WebRTCSink,
+ name: String,
+ in_caps: gst::Caps,
+ codecs: &BTreeMap<i32, Codec>,
+ ) -> (String, gst::Caps) {
+ let sink_caps = in_caps.as_ref().to_owned();
+
+ let is_video = match sink_caps.structure(0).unwrap().name() {
+ "video/x-raw" => true,
+ "audio/x-raw" => false,
+ _ => unreachable!(),
+ };
+
+ let mut payloader_caps = gst::Caps::new_empty();
+ let payloader_caps_mut = payloader_caps.make_mut();
+
+ let futs = codecs
+ .iter()
+ .filter(|(_, codec)| codec.is_video() == is_video)
+ .map(|(_, codec)| WebRTCSink::run_discovery_pipeline(element, codec, &sink_caps));
+
+ for ret in futures::future::join_all(futs).await {
+ match ret {
+ Ok(s) => {
+ payloader_caps_mut.append_structure(s);
+ }
+ Err(err) => {
+ /* We don't consider this fatal, as long as we end up with one
+ * potential codec for each input stream
+ */
+ gst::warning!(
+ CAT,
+ obj: element,
+ "Codec discovery pipeline failed: {}",
+ err
+ );
+ }
+ }
+ }
+
+ (name, payloader_caps)
+ }
+
+ async fn lookup_streams_caps(&self, element: &super::WebRTCSink) -> Result<(), Error> {
+ let codecs = self.lookup_codecs();
+ let futs: Vec<_> = self
+ .state
+ .lock()
+ .unwrap()
+ .streams
+ .iter()
+ .map(|(name, stream)| {
+ WebRTCSink::lookup_caps(
+ element,
+ name.to_owned(),
+ stream.in_caps.as_ref().unwrap().to_owned(),
+ &codecs,
+ )
+ })
+ .collect();
+
+ let caps: Vec<(String, gst::Caps)> = futures::future::join_all(futs).await;
+
+ let mut state = self.state.lock().unwrap();
+
+ for (name, caps) in caps {
+ if caps.is_empty() {
+ return Err(anyhow!("No caps found for stream {}", name));
+ }
+
+ if let Some(mut stream) = state.streams.get_mut(&name) {
+ stream.out_caps = Some(caps);
+ }
+ }
+
+ state.codecs = codecs;
+
+ Ok(())
+ }
+
+ fn gather_stats(&self) -> gst::Structure {
+ gst::Structure::from_iter(
+ "application/x-webrtcsink-stats",
+ self.state
+ .lock()
+ .unwrap()
+ .sessions
+ .iter()
+ .map(|(name, consumer)| (name.as_str(), consumer.gather_stats().to_send_value())),
+ )
+ }
+
+ fn sink_event(&self, pad: &gst::Pad, element: &super::WebRTCSink, event: gst::Event) -> bool {
+ use gst::EventView;
+
+ match event.view() {
+ EventView::Caps(e) => {
+ if let Some(caps) = pad.current_caps() {
+ if caps.is_strictly_equal(e.caps()) {
+ // Nothing changed
+ true
+ } else {
+ gst::error!(CAT, obj: pad, "Renegotiation is not supported");
+ false
+ }
+ } else {
+ gst::info!(CAT, obj: pad, "Received caps event {:?}", e);
+
+ let mut all_pads_have_caps = true;
+
+ self.state
+ .lock()
+ .unwrap()
+ .streams
+ .iter_mut()
+ .for_each(|(_, mut stream)| {
+ if stream.sink_pad.upcast_ref::<gst::Pad>() == pad {
+ stream.in_caps = Some(e.caps().to_owned());
+ } else if stream.in_caps.is_none() {
+ all_pads_have_caps = false;
+ }
+ });
+
+ if all_pads_have_caps {
+ let element_clone = element.downgrade();
+ task::spawn(async move {
+ if let Some(element) = element_clone.upgrade() {
+ let this = Self::from_instance(&element);
+ let (fut, handle) =
+ futures::future::abortable(this.lookup_streams_caps(&element));
+
+ let (codecs_done_sender, codecs_done_receiver) =
+ futures::channel::oneshot::channel();
+
+ // Compiler isn't budged by dropping state before await,
+ // so let's make a new scope instead.
+ {
+ let mut state = this.state.lock().unwrap();
+ state.codecs_abort_handle = Some(handle);
+ state.codecs_done_receiver = Some(codecs_done_receiver);
+ }
+
+ match fut.await {
+ Ok(Err(err)) => {
+ gst::error!(CAT, obj: &element, "error: {}", err);
+ gst::element_error!(
+ element,
+ gst::StreamError::CodecNotFound,
+ ["Failed to look up output caps: {}", err]
+ );
+ }
+ Ok(Ok(_)) => {
+ let mut state = this.state.lock().unwrap();
+ state.codec_discovery_done = true;
+ state.maybe_start_signaller(&element);
+ }
+ _ => (),
+ }
+
+ let _ = codecs_done_sender.send(());
+ }
+ });
+ }
+
+ gst::Pad::event_default(pad, Some(element), event)
+ }
+ }
+ _ => gst::Pad::event_default(pad, Some(element), event),
+ }
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for WebRTCSink {
+ const NAME: &'static str = "RsWebRTCSink";
+ type Type = super::WebRTCSink;
+ type ParentType = gst::Bin;
+ type Interfaces = (gst::ChildProxy, gst_video::Navigation);
+}
+
+impl ObjectImpl for WebRTCSink {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecBoxed::new(
+ "video-caps",
+ "Video encoder caps",
+ "Governs what video codecs will be proposed",
+ gst::Caps::static_type(),
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecBoxed::new(
+ "audio-caps",
+ "Audio encoder caps",
+ "Governs what audio codecs will be proposed",
+ gst::Caps::static_type(),
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecString::new(
+ "stun-server",
+ "STUN Server",
+ "The STUN server of the form stun://hostname:port",
+ DEFAULT_STUN_SERVER,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpecString::new(
+ "turn-server",
+ "TURN Server",
+ "The TURN server of the form turn(s)://username:password@host:port.",
+ None,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpecEnum::new(
+ "congestion-control",
+ "Congestion control",
+ "Defines how congestion is controlled, if at all",
+ WebRTCSinkCongestionControl::static_type(),
+ DEFAULT_CONGESTION_CONTROL as i32,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecUInt::new(
+ "min-bitrate",
+ "Minimal Bitrate",
+ "Minimal bitrate to use (in bit/sec) when computing it through the congestion control algorithm",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_MIN_BITRATE,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecUInt::new(
+ "max-bitrate",
+ "Minimal Bitrate",
+ "Minimal bitrate to use (in bit/sec) when computing it through the congestion control algorithm",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_MAX_BITRATE,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecUInt::new(
+ "start-bitrate",
+ "Start Bitrate",
+ "Start bitrate to use (in bit/sec)",
+ 1,
+ u32::MAX as u32,
+ DEFAULT_START_BITRATE,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY,
+ ),
+ glib::ParamSpecBoxed::new(
+ "stats",
+ "Consumer statistics",
+ "Statistics for the current consumers",
+ gst::Structure::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecBoolean::new(
+ "do-fec",
+ "Do Forward Error Correction",
+ "Whether the element should negotiate and send FEC data",
+ DEFAULT_DO_FEC,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY
+ ),
+ glib::ParamSpecBoolean::new(
+ "do-retransmission",
+ "Do retransmission",
+ "Whether the element should offer to honor retransmission requests",
+ DEFAULT_DO_RETRANSMISSION,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY
+ ),
+ glib::ParamSpecBoolean::new(
+ "enable-data-channel-navigation",
+ "Enable data channel navigation",
+ "Enable navigation events through a dedicated WebRTCDataChannel",
+ DEFAULT_ENABLE_DATA_CHANNEL_NAVIGATION,
+ glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_READY
+ ),
+ glib::ParamSpecBoxed::new(
+ "meta",
+ "Meta",
+ "Free form metadata about the producer",
+ gst::Structure::static_type(),
+ glib::ParamFlags::READWRITE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "video-caps" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.video_caps = value
+ .get::<Option<gst::Caps>>()
+ .expect("type checked upstream")
+ .unwrap_or_else(gst::Caps::new_empty);
+ }
+ "audio-caps" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.audio_caps = value
+ .get::<Option<gst::Caps>>()
+ .expect("type checked upstream")
+ .unwrap_or_else(gst::Caps::new_empty);
+ }
+ "stun-server" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.stun_server = value
+ .get::<Option<String>>()
+ .expect("type checked upstream")
+ }
+ "turn-server" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.turn_server = value
+ .get::<Option<String>>()
+ .expect("type checked upstream")
+ }
+ "congestion-control" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.cc_info.heuristic = value
+ .get::<WebRTCSinkCongestionControl>()
+ .expect("type checked upstream");
+ }
+ "min-bitrate" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.cc_info.min_bitrate = value.get::<u32>().expect("type checked upstream");
+ }
+ "max-bitrate" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.cc_info.max_bitrate = value.get::<u32>().expect("type checked upstream");
+ }
+ "start-bitrate" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.cc_info.start_bitrate = value.get::<u32>().expect("type checked upstream");
+ }
+ "do-fec" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.do_fec = value.get::<bool>().expect("type checked upstream");
+ }
+ "do-retransmission" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.do_retransmission = value.get::<bool>().expect("type checked upstream");
+ }
+ "enable-data-channel-navigation" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.enable_data_channel_navigation =
+ value.get::<bool>().expect("type checked upstream");
+ }
+ "meta" => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.meta = value
+ .get::<Option<gst::Structure>>()
+ .expect("type checked upstream")
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "video-caps" => {
+ let settings = self.settings.lock().unwrap();
+ settings.video_caps.to_value()
+ }
+ "audio-caps" => {
+ let settings = self.settings.lock().unwrap();
+ settings.audio_caps.to_value()
+ }
+ "congestion-control" => {
+ let settings = self.settings.lock().unwrap();
+ settings.cc_info.heuristic.to_value()
+ }
+ "stun-server" => {
+ let settings = self.settings.lock().unwrap();
+ settings.stun_server.to_value()
+ }
+ "turn-server" => {
+ let settings = self.settings.lock().unwrap();
+ settings.turn_server.to_value()
+ }
+ "min-bitrate" => {
+ let settings = self.settings.lock().unwrap();
+ settings.cc_info.min_bitrate.to_value()
+ }
+ "max-bitrate" => {
+ let settings = self.settings.lock().unwrap();
+ settings.cc_info.max_bitrate.to_value()
+ }
+ "start-bitrate" => {
+ let settings = self.settings.lock().unwrap();
+ settings.cc_info.start_bitrate.to_value()
+ }
+ "do-fec" => {
+ let settings = self.settings.lock().unwrap();
+ settings.do_fec.to_value()
+ }
+ "do-retransmission" => {
+ let settings = self.settings.lock().unwrap();
+ settings.do_retransmission.to_value()
+ }
+ "enable-data-channel-navigation" => {
+ let settings = self.settings.lock().unwrap();
+ settings.enable_data_channel_navigation.to_value()
+ }
+ "stats" => self.gather_stats().to_value(),
+ "meta" => {
+ let settings = self.settings.lock().unwrap();
+ settings.meta.to_value()
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn signals() -> &'static [glib::subclass::Signal] {
+ static SIGNALS: Lazy<Vec<glib::subclass::Signal>> = Lazy::new(|| {
+ vec![
+ /*
+ * RsWebRTCSink::consumer-added:
+ * @consumer_id: Identifier of the consumer added
+ * @webrtcbin: The new webrtcbin
+ *
+ * This signal can be used to tweak @webrtcbin, creating a data
+ * channel for example.
+ */
+ glib::subclass::Signal::builder("consumer-added")
+ .param_types([String::static_type(), gst::Element::static_type()])
+ .build(),
+ /*
+ * RsWebRTCSink::consumer_removed:
+ * @consumer_id: Identifier of the consumer that was removed
+ * @webrtcbin: The webrtcbin connected to the newly removed consumer
+ *
+ * This signal is emitted right after the connection with a consumer
+ * has been dropped.
+ */
+ glib::subclass::Signal::builder("consumer-removed")
+ .param_types([String::static_type(), gst::Element::static_type()])
+ .build(),
+ /*
+ * RsWebRTCSink::get_sessions:
+ *
+ * List all sessions (by ID).
+ */
+ glib::subclass::Signal::builder("get-sessions")
+ .action()
+ .class_handler(|_, args| {
+ let element = args[0].get::<super::WebRTCSink>().expect("signal arg");
+ let this = element.imp();
+
+ let res = Some(
+ this.state
+ .lock()
+ .unwrap()
+ .sessions
+ .keys()
+ .cloned()
+ .collect::<Vec<String>>()
+ .to_value(),
+ );
+ res
+ })
+ .return_type::<Vec<String>>()
+ .build(),
+ /*
+ * RsWebRTCSink::encoder-setup:
+ * @consumer_id: Identifier of the consumer
+ * @pad_name: The name of the corresponding input pad
+ * @encoder: The constructed encoder
+ *
+ * This signal can be used to tweak @encoder properties.
+ *
+ * Returns: True if the encoder is entirely configured,
+ * False to let other handlers run
+ */
+ glib::subclass::Signal::builder("encoder-setup")
+ .param_types([
+ String::static_type(),
+ String::static_type(),
+ gst::Element::static_type(),
+ ])
+ .return_type::<bool>()
+ .accumulator(|_hint, _ret, value| !value.get::<bool>().unwrap())
+ .class_handler(|_, args| {
+ let element = args[0].get::<super::WebRTCSink>().expect("signal arg");
+ let enc = args[3].get::<gst::Element>().unwrap();
+
+ gst::debug!(
+ CAT,
+ obj: &element,
+ "applying default configuration on encoder {:?}",
+ enc
+ );
+
+ let this = element.imp();
+ let settings = this.settings.lock().unwrap();
+ configure_encoder(&enc, settings.cc_info.start_bitrate);
+
+ // Return false here so that latter handlers get called
+ Some(false.to_value())
+ })
+ .build(),
+ ]
+ });
+
+ SIGNALS.as_ref()
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ let obj = self.instance();
+ obj.set_suppressed_flags(gst::ElementFlags::SINK | gst::ElementFlags::SOURCE);
+ obj.set_element_flags(gst::ElementFlags::SINK);
+ }
+}
+
+impl GstObjectImpl for WebRTCSink {}
+
+impl ElementImpl for WebRTCSink {
+ fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
+ static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
+ gst::subclass::ElementMetadata::new(
+ "WebRTCSink",
+ "Sink/Network/WebRTC",
+ "WebRTC sink",
+ "Mathieu Duponchelle <mathieu@centricular.com>",
+ )
+ });
+
+ Some(&*ELEMENT_METADATA)
+ }
+
+ fn pad_templates() -> &'static [gst::PadTemplate] {
+ static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
+ let caps = gst::Caps::builder_full()
+ .structure(gst::Structure::builder("video/x-raw").build())
+ .structure_with_features(
+ gst::Structure::builder("video/x-raw").build(),
+ gst::CapsFeatures::new(&[CUDA_MEMORY_FEATURE]),
+ )
+ .structure_with_features(
+ gst::Structure::builder("video/x-raw").build(),
+ gst::CapsFeatures::new(&[GL_MEMORY_FEATURE]),
+ )
+ .structure_with_features(
+ gst::Structure::builder("video/x-raw").build(),
+ gst::CapsFeatures::new(&[NVMM_MEMORY_FEATURE]),
+ )
+ .build();
+ let video_pad_template = gst::PadTemplate::new(
+ "video_%u",
+ gst::PadDirection::Sink,
+ gst::PadPresence::Request,
+ &caps,
+ )
+ .unwrap();
+
+ let caps = gst::Caps::builder("audio/x-raw").build();
+ let audio_pad_template = gst::PadTemplate::new(
+ "audio_%u",
+ gst::PadDirection::Sink,
+ gst::PadPresence::Request,
+ &caps,
+ )
+ .unwrap();
+
+ vec![video_pad_template, audio_pad_template]
+ });
+
+ PAD_TEMPLATES.as_ref()
+ }
+
+ fn request_new_pad(
+ &self,
+ templ: &gst::PadTemplate,
+ _name: Option<String>,
+ _caps: Option<&gst::Caps>,
+ ) -> Option<gst::Pad> {
+ let element = self.instance();
+ if element.current_state() > gst::State::Ready {
+ gst::error!(CAT, "element pads can only be requested before starting");
+ return None;
+ }
+
+ let mut state = self.state.lock().unwrap();
+
+ let name = if templ.name().starts_with("video_") {
+ let name = format!("video_{}", state.video_serial);
+ state.video_serial += 1;
+ name
+ } else {
+ let name = format!("audio_{}", state.audio_serial);
+ state.audio_serial += 1;
+ name
+ };
+
+ let sink_pad = gst::GhostPad::builder_with_template(templ, Some(name.as_str()))
+ .event_function(|pad, parent, event| {
+ WebRTCSink::catch_panic_pad_function(
+ parent,
+ || false,
+ |this| this.sink_event(pad.upcast_ref(), &*this.instance(), event),
+ )
+ })
+ .build();
+
+ sink_pad.set_active(true).unwrap();
+ sink_pad.use_fixed_caps();
+ element.add_pad(&sink_pad).unwrap();
+
+ state.streams.insert(
+ name,
+ InputStream {
+ sink_pad: sink_pad.clone(),
+ producer: None,
+ in_caps: None,
+ out_caps: None,
+ clocksync: None,
+ },
+ );
+
+ Some(sink_pad.upcast())
+ }
+
+ fn change_state(
+ &self,
+ transition: gst::StateChange,
+ ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
+ let element = self.instance();
+ if let gst::StateChange::ReadyToPaused = transition {
+ if let Err(err) = self.prepare(&*element) {
+ gst::element_error!(
+ element,
+ gst::StreamError::Failed,
+ ["Failed to prepare: {}", err]
+ );
+ return Err(gst::StateChangeError);
+ }
+ }
+
+ let mut ret = self.parent_change_state(transition);
+
+ match transition {
+ gst::StateChange::PausedToReady => {
+ if let Err(err) = self.unprepare(&*element) {
+ gst::element_error!(
+ element,
+ gst::StreamError::Failed,
+ ["Failed to unprepare: {}", err]
+ );
+ return Err(gst::StateChangeError);
+ }
+ }
+ gst::StateChange::ReadyToPaused => {
+ ret = Ok(gst::StateChangeSuccess::NoPreroll);
+ }
+ gst::StateChange::PausedToPlaying => {
+ let mut state = self.state.lock().unwrap();
+ state.maybe_start_signaller(&*element);
+ }
+ _ => (),
+ }
+
+ ret
+ }
+}
+
+impl BinImpl for WebRTCSink {}
+
+impl ChildProxyImpl for WebRTCSink {
+ fn child_by_index(&self, _index: u32) -> Option<glib::Object> {
+ None
+ }
+
+ fn children_count(&self) -> u32 {
+ 0
+ }
+
+ fn child_by_name(&self, name: &str) -> Option<glib::Object> {
+ match name {
+ "signaller" => Some(
+ self.state
+ .lock()
+ .unwrap()
+ .signaller
+ .as_ref()
+ .as_ref()
+ .clone(),
+ ),
+ _ => None,
+ }
+ }
+}
+
+impl NavigationImpl for WebRTCSink {
+ fn send_event(&self, event_def: gst::Structure) {
+ let mut state = self.state.lock().unwrap();
+ let event = gst::event::Navigation::new(event_def);
+
+ state.streams.iter_mut().for_each(|(_, stream)| {
+ if stream.sink_pad.name().starts_with("video_") {
+ gst::log!(CAT, "Navigating to: {:?}", event);
+ // FIXME: Handle multi tracks.
+ if !stream.sink_pad.push_event(event.clone()) {
+ gst::info!(CAT, "Could not send event: {:?}", event);
+ }
+ }
+ });
+ }
+}
diff --git a/net/webrtc/plugins/src/webrtcsink/mod.rs b/net/webrtc/plugins/src/webrtcsink/mod.rs
new file mode 100644
index 00000000..9d6913c9
--- /dev/null
+++ b/net/webrtc/plugins/src/webrtcsink/mod.rs
@@ -0,0 +1,164 @@
+use gst::glib;
+use gst::prelude::*;
+use gst::subclass::prelude::*;
+use std::error::Error;
+
+mod homegrown_cc;
+mod imp;
+
+glib::wrapper! {
+ pub struct WebRTCSink(ObjectSubclass<imp::WebRTCSink>) @extends gst::Bin, gst::Element, gst::Object, @implements gst::ChildProxy, gst_video::Navigation;
+}
+
+unsafe impl Send for WebRTCSink {}
+unsafe impl Sync for WebRTCSink {}
+
+#[derive(thiserror::Error, Debug)]
+pub enum WebRTCSinkError {
+ #[error("no session with id")]
+ NoSessionWithId(String),
+ #[error("consumer refused media")]
+ ConsumerRefusedMedia { session_id: String, media_idx: u32 },
+ #[error("consumer did not provide valid payload for media")]
+ ConsumerNoValidPayload { session_id: String, media_idx: u32 },
+ #[error("SDP mline index is currently mandatory")]
+ MandatorySdpMlineIndex,
+ #[error("duplicate session id")]
+ DuplicateSessionId(String),
+ #[error("error setting up consumer pipeline")]
+ SessionPipelineError {
+ session_id: String,
+ peer_id: String,
+ details: String,
+ },
+}
+
+pub trait Signallable: Sync + Send + 'static {
+ fn start(&mut self, element: &WebRTCSink) -> Result<(), Box<dyn Error>>;
+
+ fn handle_sdp(
+ &mut self,
+ element: &WebRTCSink,
+ session_id: &str,
+ sdp: &gst_webrtc::WebRTCSessionDescription,
+ ) -> Result<(), Box<dyn Error>>;
+
+ /// sdp_mid is exposed for future proofing, see
+ /// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1174,
+ /// at the moment sdp_m_line_index will always be Some and sdp_mid will always
+ /// be None
+ fn handle_ice(
+ &mut self,
+ element: &WebRTCSink,
+ session_id: &str,
+ candidate: &str,
+ sdp_m_line_index: Option<u32>,
+ sdp_mid: Option<String>,
+ ) -> Result<(), Box<dyn Error>>;
+
+ fn session_ended(&mut self, element: &WebRTCSink, session_id: &str);
+
+ fn stop(&mut self, element: &WebRTCSink);
+}
+
+/// When providing a signaller, we expect it to both be a GObject
+/// and be Signallable. This is arguably a bit strange, but exposing
+/// a GInterface from rust is at the moment a bit awkward, so I went
+/// for a rust interface for now. The reason the signaller needs to be
+/// a GObject is to make its properties available through the GstChildProxy
+/// interface.
+pub trait SignallableObject: AsRef<glib::Object> + Signallable {}
+
+impl<T: AsRef<glib::Object> + Signallable> SignallableObject for T {}
+
+impl Default for WebRTCSink {
+ fn default() -> Self {
+ glib::Object::new::<Self>(&[])
+ }
+}
+
+impl WebRTCSink {
+ pub fn with_signaller(signaller: Box<dyn SignallableObject>) -> Self {
+ let ret: WebRTCSink = glib::Object::new(&[]);
+
+ let ws = imp::WebRTCSink::from_instance(&ret);
+
+ ws.set_signaller(signaller).unwrap();
+
+ ret
+ }
+
+ pub fn handle_sdp(
+ &self,
+ session_id: &str,
+ sdp: &gst_webrtc::WebRTCSessionDescription,
+ ) -> Result<(), WebRTCSinkError> {
+ let ws = imp::WebRTCSink::from_instance(self);
+
+ ws.handle_sdp(self, session_id, sdp)
+ }
+
+ /// sdp_mid is exposed for future proofing, see
+ /// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1174,
+ /// at the moment sdp_m_line_index must be Some
+ pub fn handle_ice(
+ &self,
+ session_id: &str,
+ sdp_m_line_index: Option<u32>,
+ sdp_mid: Option<String>,
+ candidate: &str,
+ ) -> Result<(), WebRTCSinkError> {
+ let ws = imp::WebRTCSink::from_instance(self);
+
+ ws.handle_ice(self, session_id, sdp_m_line_index, sdp_mid, candidate)
+ }
+
+ pub fn handle_signalling_error(&self, error: Box<dyn Error + Send + Sync>) {
+ let ws = imp::WebRTCSink::from_instance(self);
+
+ ws.handle_signalling_error(self, anyhow::anyhow!(error));
+ }
+
+ pub fn start_session(&self, session_id: &str, peer_id: &str) -> Result<(), WebRTCSinkError> {
+ let ws = imp::WebRTCSink::from_instance(self);
+
+ ws.start_session(self, session_id, peer_id)
+ }
+
+ pub fn end_session(&self, session_id: &str) -> Result<(), WebRTCSinkError> {
+ let ws = imp::WebRTCSink::from_instance(self);
+
+ ws.remove_session(self, session_id, false)
+ }
+}
+
+#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
+#[repr(u32)]
+#[enum_type(name = "GstWebRTCSinkCongestionControl")]
+pub enum WebRTCSinkCongestionControl {
+ #[enum_value(name = "Disabled: no congestion control is applied", nick = "disabled")]
+ Disabled,
+ #[enum_value(name = "Homegrown: simple sender-side heuristic", nick = "homegrown")]
+ Homegrown,
+ #[enum_value(name = "Google Congestion Control algorithm", nick = "gcc")]
+ GoogleCongestionControl,
+}
+
+#[glib::flags(name = "GstWebRTCSinkMitigationMode")]
+enum WebRTCSinkMitigationMode {
+ #[flags_value(name = "No mitigation applied", nick = "none")]
+ NONE = 0b00000000,
+ #[flags_value(name = "Lowered resolution", nick = "downscaled")]
+ DOWNSCALED = 0b00000001,
+ #[flags_value(name = "Lowered framerate", nick = "downsampled")]
+ DOWNSAMPLED = 0b00000010,
+}
+
+pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ gst::Element::register(
+ Some(plugin),
+ "webrtcsink",
+ gst::Rank::None,
+ WebRTCSink::static_type(),
+ )
+}
diff --git a/net/webrtc/protocol/Cargo.toml b/net/webrtc/protocol/Cargo.toml
new file mode 100644
index 00000000..e3e3099c
--- /dev/null
+++ b/net/webrtc/protocol/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name="webrtcsink-protocol"
+version = "0.1.0"
+edition = "2018"
+authors = ["Mathieu Duponchelle <mathieu@centricular.com>"]
+license = "MPL-2.0"
+description = "GStreamer WebRTC sink default protocol"
+repository = "https://github.com/centricular/webrtcsink/"
+
+[dependencies]
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
diff --git a/net/webrtc/protocol/src/lib.rs b/net/webrtc/protocol/src/lib.rs
new file mode 100644
index 00000000..9af9e929
--- /dev/null
+++ b/net/webrtc/protocol/src/lib.rs
@@ -0,0 +1,144 @@
+/// The default protocol used by the signalling server
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct Peer {
+ pub id: String,
+ #[serde(default)]
+ pub meta: Option<serde_json::Value>,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[serde(tag = "type")]
+#[serde(rename_all = "camelCase")]
+/// Messages sent from the server to peers
+pub enum OutgoingMessage {
+ /// Welcoming message, sets the Peer ID linked to a new connection
+ Welcome { peer_id: String },
+ /// Notifies listeners that a peer status has changed
+ PeerStatusChanged(PeerStatus),
+ /// Instructs a peer to generate an offer and inform about the session ID
+ #[serde(rename_all = "camelCase")]
+ StartSession { peer_id: String, session_id: String },
+ /// Let consumer know that the requested session is starting with the specified identifier
+ #[serde(rename_all = "camelCase")]
+ SessionStarted { peer_id: String, session_id: String },
+ /// Signals that the session the peer was in was ended
+ #[serde(rename_all = "camelCase")]
+ EndSession(EndSessionMessage),
+ /// Messages directly forwarded from one peer to another
+ Peer(PeerMessage),
+ /// Provides the current list of consumer peers
+ List { producers: Vec<Peer> },
+ /// Notifies that an error occurred with the peer's current session
+ Error { details: String },
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
+#[serde(rename_all = "camelCase")]
+/// Register with a peer type
+pub enum PeerRole {
+ /// Register as a producer
+ #[serde(rename_all = "camelCase")]
+ Producer,
+ /// Register as a listener
+ #[serde(rename_all = "camelCase")]
+ Listener,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct PeerStatus {
+ pub roles: Vec<PeerRole>,
+ pub meta: Option<serde_json::Value>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(default)]
+ pub peer_id: Option<String>,
+}
+
+impl PeerStatus {
+ pub fn producing(&self) -> bool {
+ self.roles.iter().any(|t| matches!(t, PeerRole::Producer))
+ }
+
+ pub fn listening(&self) -> bool {
+ self.roles.iter().any(|t| matches!(t, PeerRole::Listener))
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+/// Ask the server to start a session with a producer peer
+pub struct StartSessionMessage {
+ /// Identifies the peer
+ pub peer_id: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[serde(tag = "type")]
+#[serde(rename_all = "camelCase")]
+/// Conveys a SDP
+pub enum SdpMessage {
+ /// Conveys an offer
+ Offer {
+ /// The SDP
+ sdp: String,
+ },
+ /// Conveys an answer
+ Answer {
+ /// The SDP
+ sdp: String,
+ },
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[serde(rename_all = "camelCase")]
+/// Contents of the peer message
+pub enum PeerMessageInner {
+ /// Conveys an ICE candidate
+ #[serde(rename_all = "camelCase")]
+ Ice {
+ /// The candidate string
+ candidate: String,
+ /// The mline index the candidate applies to
+ sdp_m_line_index: u32,
+ },
+ Sdp(SdpMessage),
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[serde(rename_all = "camelCase")]
+/// Messages directly forwarded from one peer to another
+pub struct PeerMessage {
+ pub session_id: String,
+ #[serde(flatten)]
+ pub peer_message: PeerMessageInner,
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+/// End a session
+pub struct EndSessionMessage {
+ /// The identifier of the session to end
+ pub session_id: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(tag = "type")]
+#[serde(rename_all = "camelCase")]
+/// Messages received by the server from peers
+pub enum IncomingMessage {
+ /// Internal message to let know about new peers
+ NewPeer,
+ /// Set current peer status
+ SetPeerStatus(PeerStatus),
+ /// Start a session with a producer peer
+ StartSession(StartSessionMessage),
+ /// End an existing session
+ EndSession(EndSessionMessage),
+ /// Send a message to a peer the sender is currently in session with
+ Peer(PeerMessage),
+ /// Retrieve the current list of producers
+ List,
+}
diff --git a/net/webrtc/signalling/Cargo.toml b/net/webrtc/signalling/Cargo.toml
new file mode 100644
index 00000000..986c47f3
--- /dev/null
+++ b/net/webrtc/signalling/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name="webrtcsink-signalling"
+version = "0.1.0"
+edition = "2018"
+authors = ["Mathieu Duponchelle <mathieu@centricular.com>"]
+license = "MPL-2.0"
+description = "GStreamer WebRTC sink signalling server"
+repository = "https://github.com/centricular/webrtcsink/"
+
+[dependencies]
+anyhow = "1"
+async-std = { version = "1", features = ["unstable", "attributes"] }
+async-native-tls = "0.4"
+async-tungstenite = { version = "0.17", features = ["async-std-runtime", "async-native-tls"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+clap = { version = "4", features = ["derive"] }
+tracing = { version = "0.1", features = ["log"] }
+tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
+tracing-log = "0.1"
+futures = "0.3"
+uuid = { version = "1", features = ["v4"] }
+thiserror = "1"
+test-log = { version = "0.2", features = ["trace"], default-features = false }
+pin-project-lite = "0.2"
+webrtcsink-protocol = { version = "0.1", path="../protocol" }
diff --git a/net/webrtc/signalling/src/bin/server.rs b/net/webrtc/signalling/src/bin/server.rs
new file mode 100644
index 00000000..65f41055
--- /dev/null
+++ b/net/webrtc/signalling/src/bin/server.rs
@@ -0,0 +1,101 @@
+use async_std::task;
+use clap::Parser;
+use tracing_subscriber::prelude::*;
+use webrtcsink_signalling::handlers::Handler;
+use webrtcsink_signalling::server::Server;
+
+use anyhow::Error;
+use async_native_tls::TlsAcceptor;
+use async_std::fs::File as AsyncFile;
+use async_std::net::TcpListener;
+use tracing::{info, warn};
+
+#[derive(Parser, Debug)]
+#[clap(about, version, author)]
+/// Program arguments
+struct Args {
+ /// Address to listen on
+ #[clap(short, long, default_value = "0.0.0.0")]
+ host: String,
+ /// Port to listen on
+ #[clap(short, long, default_value_t = 8443)]
+ port: u16,
+ /// TLS certificate to use
+ #[clap(short, long)]
+ cert: Option<String>,
+ /// password to TLS certificate
+ #[clap(long)]
+ cert_password: Option<String>,
+}
+
+fn initialize_logging(envvar_name: &str) -> Result<(), Error> {
+ tracing_log::LogTracer::init()?;
+ let env_filter = tracing_subscriber::EnvFilter::try_from_env(envvar_name)
+ .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
+ let fmt_layer = tracing_subscriber::fmt::layer()
+ .with_thread_ids(true)
+ .with_target(true)
+ .with_span_events(
+ tracing_subscriber::fmt::format::FmtSpan::NEW
+ | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
+ );
+ let subscriber = tracing_subscriber::Registry::default()
+ .with(env_filter)
+ .with(fmt_layer);
+ tracing::subscriber::set_global_default(subscriber)?;
+
+ Ok(())
+}
+
+fn main() -> Result<(), Error> {
+ let args = Args::parse();
+ let server = Server::spawn(|stream| Handler::new(stream));
+
+ initialize_logging("WEBRTCSINK_SIGNALLING_SERVER_LOG")?;
+
+ task::block_on(async move {
+ let addr = format!("{}:{}", args.host, args.port);
+
+ // Create the event loop and TCP listener we'll accept connections on.
+ let listener = TcpListener::bind(&addr).await?;
+
+ let acceptor = match args.cert {
+ Some(cert) => {
+ let key = AsyncFile::open(cert).await?;
+ Some(TlsAcceptor::new(key, args.cert_password.as_deref().unwrap_or("")).await?)
+ }
+ None => None,
+ };
+
+ info!("Listening on: {}", addr);
+
+ while let Ok((stream, _)) = listener.accept().await {
+ let mut server_clone = server.clone();
+
+ let address = match stream.peer_addr() {
+ Ok(address) => address,
+ Err(err) => {
+ warn!("Connected peer with no address: {}", err);
+ continue;
+ }
+ };
+
+ info!("Accepting connection from {}", address);
+
+ if let Some(ref acceptor) = acceptor {
+ let stream = match acceptor.accept(stream).await {
+ Ok(stream) => stream,
+ Err(err) => {
+ warn!("Failed to accept TLS connection from {}: {}", address, err);
+ continue;
+ }
+ };
+ task::spawn(async move { server_clone.accept_async(stream).await });
+ } else {
+ task::spawn(async move { server_clone.accept_async(stream).await });
+ }
+ }
+
+ Ok(())
+ })
+}
diff --git a/net/webrtc/signalling/src/handlers/mod.rs b/net/webrtc/signalling/src/handlers/mod.rs
new file mode 100644
index 00000000..011bf910
--- /dev/null
+++ b/net/webrtc/signalling/src/handlers/mod.rs
@@ -0,0 +1,1421 @@
+use anyhow::{anyhow, Error};
+use anyhow::{bail, Context};
+use futures::prelude::*;
+use futures::ready;
+use p::PeerStatus;
+use pin_project_lite::pin_project;
+use std::collections::{HashMap, HashSet, VecDeque};
+use std::pin::Pin;
+use std::task::{Context as TaskContext, Poll};
+use tracing::log::error;
+use tracing::{info, instrument, warn};
+use webrtcsink_protocol as p;
+
+type PeerId = String;
+
+#[derive(Clone)]
+struct Session {
+ id: String,
+ producer: PeerId,
+ consumer: PeerId,
+}
+
+impl Session {
+ fn other_peer_id(&self, id: &str) -> Result<&str, Error> {
+ if self.producer == id {
+ Ok(&self.consumer)
+ } else if self.consumer == id {
+ Ok(&self.producer)
+ } else {
+ bail!("Peer {id} is not part of {}", self.id)
+ }
+ }
+}
+
+pin_project! {
+ #[must_use = "streams do nothing unless polled"]
+ pub struct Handler {
+ #[pin]
+ stream: Pin<Box<dyn Stream<Item=(String, Option<p::IncomingMessage>)> + Send>>,
+ items: VecDeque<(String, p::OutgoingMessage)>,
+ peers: HashMap<PeerId, PeerStatus>,
+ sessions: HashMap<String, Session>,
+ consumer_sessions: HashMap<String, HashSet<String>>,
+ producer_sessions: HashMap<String, HashSet<String>>,
+ }
+}
+
+impl Handler {
+ #[instrument(level = "debug", skip(stream))]
+ /// Create a handler
+ pub fn new(
+ stream: Pin<Box<dyn Stream<Item = (String, Option<p::IncomingMessage>)> + Send>>,
+ ) -> Self {
+ Self {
+ stream,
+ items: VecDeque::new(),
+ peers: Default::default(),
+ sessions: Default::default(),
+ consumer_sessions: Default::default(),
+ producer_sessions: Default::default(),
+ }
+ }
+
+ #[instrument(level = "trace", skip(self))]
+ fn handle(
+ mut self: Pin<&mut Self>,
+ peer_id: &str,
+ msg: p::IncomingMessage,
+ ) -> Result<(), Error> {
+ match msg {
+ p::IncomingMessage::NewPeer => {
+ self.peers.insert(peer_id.to_string(), Default::default());
+ self.items.push_back((
+ peer_id.into(),
+ p::OutgoingMessage::Welcome {
+ peer_id: peer_id.to_string(),
+ },
+ ));
+
+ Ok(())
+ }
+ p::IncomingMessage::SetPeerStatus(status) => self.set_peer_status(peer_id, &status),
+ p::IncomingMessage::StartSession(message) => {
+ self.start_session(&message.peer_id, peer_id)
+ }
+ p::IncomingMessage::Peer(peermsg) => self.handle_peer_message(peer_id, peermsg),
+ p::IncomingMessage::List => self.list_producers(peer_id),
+ p::IncomingMessage::EndSession(msg) => self.end_session(peer_id, &msg.session_id),
+ }
+ }
+
+ fn handle_peer_message(&mut self, peer_id: &str, peermsg: p::PeerMessage) -> Result<(), Error> {
+ let session_id = &peermsg.session_id;
+ let session = self
+ .sessions
+ .get(session_id)
+ .context(format!("Session {} doesn't exist", session_id))?
+ .clone();
+
+ if matches!(
+ peermsg.peer_message,
+ p::PeerMessageInner::Sdp(p::SdpMessage::Offer { .. })
+ ) && peer_id == session.consumer
+ {
+ bail!(
+ r#"cannot forward offer from "{peer_id}" to "{}" as "{peer_id}" is not the producer"#,
+ session.producer,
+ );
+ }
+
+ self.items.push_back((
+ session.other_peer_id(peer_id)?.to_owned(),
+ p::OutgoingMessage::Peer(p::PeerMessage {
+ session_id: session_id.to_string(),
+ peer_message: peermsg.peer_message.clone(),
+ }),
+ ));
+
+ Ok(())
+ }
+
+ fn stop_producer(&mut self, peer_id: &str) {
+ if let Some(session_ids) = self.producer_sessions.remove(peer_id) {
+ for session_id in session_ids {
+ if let Err(e) = self.end_session(peer_id, &session_id) {
+ error!("Could not end session {session_id}: {e:?}");
+ }
+ }
+ }
+ }
+
+ fn stop_consumer(&mut self, peer_id: &str) {
+ if let Some(session_ids) = self.consumer_sessions.remove(peer_id) {
+ for session_id in session_ids {
+ if let Err(e) = self.end_session(peer_id, &session_id) {
+ error!("Could not end session {session_id}: {e:?}");
+ }
+ }
+ }
+ }
+
+ #[instrument(level = "debug", skip(self))]
+ /// Remove a peer, this can cause sessions to be ended
+ fn remove_peer(&mut self, peer_id: &str) {
+ info!(peer_id = %peer_id, "removing peer");
+ let peer_status = match self.peers.remove(peer_id) {
+ Some(peer_status) => peer_status,
+ _ => return,
+ };
+
+ self.stop_producer(peer_id);
+ self.stop_consumer(peer_id);
+
+ for (id, p) in self.peers.iter() {
+ if !p.listening() {
+ continue;
+ }
+
+ let message = p::OutgoingMessage::PeerStatusChanged(PeerStatus {
+ roles: Default::default(),
+ meta: peer_status.meta.clone(),
+ peer_id: Some(peer_id.to_string()),
+ });
+ self.items.push_back((id.to_string(), message));
+ }
+ }
+
+ #[instrument(level = "debug", skip(self))]
+ /// End a session between two peers
+ fn end_session(&mut self, peer_id: &str, session_id: &str) -> Result<(), Error> {
+ let session = self
+ .sessions
+ .remove(session_id)
+ .with_context(|| format!("Session {session_id} doesn't exist"))?;
+
+ self.consumer_sessions
+ .entry(session.consumer.clone())
+ .and_modify(|sessions| {
+ sessions.remove(session_id);
+ });
+
+ self.producer_sessions
+ .entry(session.producer.clone())
+ .and_modify(|sessions| {
+ sessions.remove(session_id);
+ });
+
+ self.items.push_back((
+ session.other_peer_id(peer_id)?.to_string(),
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.to_string(),
+ }),
+ ));
+
+ Ok(())
+ }
+
+ /// List producer peers
+ #[instrument(level = "debug", skip(self))]
+ fn list_producers(&mut self, peer_id: &str) -> Result<(), Error> {
+ self.items.push_back((
+ peer_id.to_string(),
+ p::OutgoingMessage::List {
+ producers: self
+ .peers
+ .iter()
+ .filter_map(|(peer_id, peer)| {
+ peer.producing().then_some(p::Peer {
+ id: peer_id.clone(),
+ meta: peer.meta.clone(),
+ })
+ })
+ .collect(),
+ },
+ ));
+
+ Ok(())
+ }
+
+ /// Register peer as a producer
+ #[instrument(level = "debug", skip(self))]
+ fn set_peer_status(&mut self, peer_id: &str, status: &p::PeerStatus) -> Result<(), Error> {
+ let old_status = self
+ .peers
+ .get(peer_id)
+ .context(anyhow!("Peer '{peer_id}' hasn't been welcomed"))?;
+
+ if status == old_status {
+ info!("Status for '{}' hasn't changed", peer_id);
+
+ return Ok(());
+ }
+
+ if old_status.producing() && !status.producing() {
+ self.stop_producer(peer_id);
+ }
+
+ let mut status = status.clone();
+ status.peer_id = Some(peer_id.to_string());
+ self.peers.insert(peer_id.to_string(), status.clone());
+ for (id, peer) in &self.peers {
+ if !peer.listening() {
+ continue;
+ }
+
+ self.items.push_back((
+ id.to_string(),
+ p::OutgoingMessage::PeerStatusChanged(p::PeerStatus {
+ peer_id: Some(peer_id.to_string()),
+ roles: status.roles.clone(),
+ meta: status.meta.clone(),
+ }),
+ ));
+ }
+
+ info!(peer_id = %peer_id, "registered as a producer");
+
+ Ok(())
+ }
+
+ /// Start a session between two peers
+ #[instrument(level = "debug", skip(self))]
+ fn start_session(&mut self, producer_id: &str, consumer_id: &str) -> Result<(), Error> {
+ self.peers.get(producer_id).map_or_else(
+ || Err(anyhow!("Peer '{producer_id}' hasn't been welcomed")),
+ |peer| {
+ if !peer.producing() {
+ Err(anyhow!(
+ "Peer with id {} is not registered as a producer",
+ producer_id
+ ))
+ } else {
+ Ok(peer)
+ }
+ },
+ )?;
+
+ self.peers.get(consumer_id).map_or_else(
+ || Err(anyhow!("Peer '{consumer_id}' hasn't been welcomed")),
+ Ok,
+ )?;
+
+ let session_id = uuid::Uuid::new_v4().to_string();
+ self.sessions.insert(
+ session_id.clone(),
+ Session {
+ id: session_id.clone(),
+ consumer: consumer_id.to_string(),
+ producer: producer_id.to_string(),
+ },
+ );
+ self.consumer_sessions
+ .entry(consumer_id.to_string())
+ .or_insert(HashSet::new())
+ .insert(session_id.clone());
+ self.producer_sessions
+ .entry(producer_id.to_string())
+ .or_insert(HashSet::new())
+ .insert(session_id.clone());
+ self.items.push_back((
+ consumer_id.to_string(),
+ p::OutgoingMessage::SessionStarted {
+ peer_id: producer_id.to_string(),
+ session_id: session_id.clone(),
+ },
+ ));
+ self.items.push_back((
+ producer_id.to_string(),
+ p::OutgoingMessage::StartSession {
+ peer_id: consumer_id.to_string(),
+ session_id: session_id.clone(),
+ },
+ ));
+
+ info!(id = %session_id, producer_id = %producer_id, consumer_id = %consumer_id, "started a session");
+
+ Ok(())
+ }
+}
+
+impl Stream for Handler {
+ type Item = (String, p::OutgoingMessage);
+
+ fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
+ loop {
+ let this = self.as_mut().project();
+
+ if let Some(item) = this.items.pop_front() {
+ break Poll::Ready(Some(item));
+ }
+
+ match ready!(this.stream.poll_next(cx)) {
+ Some((peer_id, msg)) => {
+ if let Some(msg) = msg {
+ if let Err(err) = self.as_mut().handle(&peer_id, msg) {
+ self.items.push_back((
+ peer_id.to_string(),
+ p::OutgoingMessage::Error {
+ details: err.to_string(),
+ },
+ ));
+ }
+ } else {
+ self.remove_peer(&peer_id);
+ }
+ }
+ None => {
+ break Poll::Ready(None);
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use futures::channel::mpsc;
+ use serde_json::json;
+
+ async fn new_peer(
+ tx: &mut mpsc::UnboundedSender<(String, Option<p::IncomingMessage>)>,
+ handler: &mut Handler,
+ peer_id: &str,
+ ) {
+ tx.send((peer_id.to_string(), Some(p::IncomingMessage::NewPeer)))
+ .await
+ .unwrap();
+
+ let res = handler.next().await.unwrap();
+ assert_eq!(
+ res,
+ (
+ peer_id.to_string(),
+ p::OutgoingMessage::Welcome {
+ peer_id: peer_id.to_string()
+ }
+ )
+ );
+ }
+
+ #[async_std::test]
+ async fn test_register_producer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ tx.send((
+ "producer".to_string(),
+ Some(p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ })),
+ ))
+ .await
+ .unwrap();
+ }
+
+ #[async_std::test]
+ async fn test_list_producers() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ meta: Some(json!({"display-name":"foobar".to_string()})),
+ roles: vec![p::PeerRole::Producer],
+ peer_id: None,
+ });
+
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let message = p::IncomingMessage::List;
+ tx.send(("listener".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "listener");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::List {
+ producers: vec![p::Peer {
+ id: "producer".to_string(),
+ meta: Some(json!(
+ {"display-name": "foobar".to_string()
+ })),
+ }]
+ }
+ );
+ }
+
+ #[async_std::test]
+ async fn test_welcome() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+ }
+
+ #[async_std::test]
+ async fn test_listener() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+ new_peer(&mut tx, &mut handler, "listener").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Listener],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("listener".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: Some(json!({
+ "display-name": "foobar".to_string(),
+ })),
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "listener");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::PeerStatusChanged(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ peer_id: Some("producer".to_string()),
+ meta: Some(json!({
+ "display-name": Some("foobar".to_string()),
+ }
+ ))
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_start_session() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing {:?}", sent_message),
+ };
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::StartSession {
+ peer_id: "consumer".to_string(),
+ session_id: session_id.to_string(),
+ }
+ );
+ }
+
+ #[async_std::test]
+ async fn test_remove_peer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ assert_eq!(
+ handler.next().await.unwrap(),
+ (
+ "producer".into(),
+ p::OutgoingMessage::StartSession {
+ peer_id: "consumer".into(),
+ session_id: session_id.clone()
+ }
+ )
+ );
+
+ new_peer(&mut tx, &mut handler, "listener").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Listener],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("listener".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let _ = handler.next().await.unwrap();
+
+ handler.remove_peer("producer");
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage { session_id })
+ );
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "listener");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::PeerStatusChanged(PeerStatus {
+ roles: vec![],
+ peer_id: Some("producer".to_string()),
+ meta: Default::default()
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_end_session_consumer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ });
+
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_disconnect_consumer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ tx.send(("consumer".to_string(), None)).await.unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_end_session_producer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage { session_id })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_end_session_twice() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ // The consumer ends the session
+ let message = p::IncomingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone()
+ })
+ );
+
+ let message = p::IncomingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Error {
+ details: format!("Session {session_id} doesn't exist")
+ }
+ );
+ }
+
+ #[async_std::test]
+ async fn test_sdp_exchange() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Sdp(p::SdpMessage::Offer {
+ sdp: "offer".to_string(),
+ }),
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Sdp(p::SdpMessage::Offer {
+ sdp: "offer".to_string()
+ })
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_ice_exchange() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Ice {
+ candidate: "candidate".to_string(),
+ sdp_m_line_index: 42,
+ },
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Ice {
+ candidate: "candidate".to_string(),
+ sdp_m_line_index: 42
+ }
+ })
+ );
+
+ let message = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Ice {
+ candidate: "candidate".to_string(),
+ sdp_m_line_index: 42,
+ },
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Peer(p::PeerMessage {
+ session_id: session_id.clone(),
+ peer_message: p::PeerMessageInner::Ice {
+ candidate: "candidate".to_string(),
+ sdp_m_line_index: 42
+ }
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_sdp_exchange_wrong_direction_offer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::Peer(p::PeerMessage {
+ session_id,
+ peer_message: p::PeerMessageInner::Sdp(p::SdpMessage::Offer {
+ sdp: "offer".to_string(),
+ }),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let response = handler.next().await.unwrap();
+
+ assert_eq!(response,
+ (
+ "consumer".into(),
+ p::OutgoingMessage::Error {
+ details: r#"cannot forward offer from "consumer" to "producer" as "consumer" is not the producer"#.into()
+ }
+ )
+ );
+ }
+
+ #[async_std::test]
+ async fn test_start_session_no_producer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Error {
+ details: "Peer 'producer' hasn't been welcomed".into()
+ }
+ );
+ }
+
+ #[async_std::test]
+ async fn test_stop_producing() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::StartSession {
+ peer_id: "consumer".to_string(),
+ session_id: session_id.clone(),
+ }
+ );
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ })
+ );
+ }
+
+ #[async_std::test]
+ async fn test_unregistering_with_listeners() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "listener").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Listener],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("listener".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let _ = handler.next().await.unwrap();
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "listener");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::PeerStatusChanged(PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ peer_id: Some("producer".to_string()),
+ meta: Default::default()
+ })
+ );
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing {:?}", sent_message),
+ };
+
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "producer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::StartSession {
+ peer_id: "consumer".to_string(),
+ session_id: session_id.clone(),
+ }
+ );
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ assert_eq!(
+ handler.next().await.unwrap(),
+ (
+ "consumer".into(),
+ p::OutgoingMessage::EndSession(p::EndSessionMessage {
+ session_id: session_id.clone(),
+ })
+ )
+ );
+
+ assert_eq!(
+ handler.next().await.unwrap(),
+ (
+ "listener".into(),
+ p::OutgoingMessage::PeerStatusChanged(PeerStatus {
+ roles: vec![],
+ peer_id: Some("producer".to_string()),
+ meta: Default::default()
+ })
+ )
+ );
+ }
+
+ #[async_std::test]
+ async fn test_start_session_no_consumer() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+
+ assert_eq!(peer_id, "consumer");
+ assert_eq!(
+ sent_message,
+ p::OutgoingMessage::Error {
+ details: "Peer 'consumer' hasn't been welcomed".into()
+ }
+ );
+ }
+
+ #[async_std::test]
+ async fn test_start_session_twice() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ meta: Some(json!( {"display-name": "foobar".to_string() })),
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ new_peer(&mut tx, &mut handler, "consumer").await;
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session0_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let _ = handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+
+ tx.send(("consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "consumer");
+ let session1_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ assert_ne!(session0_id, session1_id);
+ }
+
+ #[async_std::test]
+ async fn test_start_session_stop_producing() {
+ let (mut tx, rx) = mpsc::unbounded();
+ let mut handler = Handler::new(Box::pin(rx));
+
+ new_peer(&mut tx, &mut handler, "producer").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer, p::PeerRole::Listener],
+ meta: None,
+ peer_id: None,
+ });
+ tx.send(("producer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let _ = handler.next().await.unwrap();
+
+ new_peer(&mut tx, &mut handler, "producer-consumer").await;
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Producer],
+ ..Default::default()
+ });
+ tx.send(("producer-consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::StartSession(p::StartSessionMessage {
+ peer_id: "producer".to_string(),
+ });
+ tx.send(("producer-consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ let (peer_id, sent_message) = handler.next().await.unwrap();
+ assert_eq!(peer_id, "producer-consumer");
+ let session0_id = match sent_message {
+ p::OutgoingMessage::SessionStarted {
+ ref peer_id,
+ ref session_id,
+ } => {
+ assert_eq!(peer_id, "producer");
+ session_id.to_string()
+ }
+ _ => panic!("SessionStarted message missing"),
+ };
+
+ let message = p::IncomingMessage::SetPeerStatus(p::PeerStatus {
+ roles: vec![p::PeerRole::Listener],
+ ..Default::default()
+ });
+ tx.send(("producer-consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+ handler.next().await.unwrap();
+
+ let message = p::IncomingMessage::List;
+ tx.send(("producer-consumer".to_string(), Some(message)))
+ .await
+ .unwrap();
+
+ handler.next().await.unwrap();
+ handler
+ .sessions
+ .get(&session0_id)
+ .expect("Session should remain");
+ }
+}
diff --git a/net/webrtc/signalling/src/lib.rs b/net/webrtc/signalling/src/lib.rs
new file mode 100644
index 00000000..068798b4
--- /dev/null
+++ b/net/webrtc/signalling/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod handlers;
+pub mod server;
diff --git a/net/webrtc/signalling/src/server/mod.rs b/net/webrtc/signalling/src/server/mod.rs
new file mode 100644
index 00000000..8ece1efd
--- /dev/null
+++ b/net/webrtc/signalling/src/server/mod.rs
@@ -0,0 +1,218 @@
+use anyhow::Error;
+use async_std::task;
+use async_tungstenite::tungstenite::Message as WsMessage;
+use futures::channel::mpsc;
+use futures::prelude::*;
+use futures::{AsyncRead, AsyncWrite};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use tracing::{info, instrument, trace, warn};
+
+struct Peer {
+ receive_task_handle: task::JoinHandle<()>,
+ send_task_handle: task::JoinHandle<Result<(), Error>>,
+ sender: mpsc::Sender<String>,
+}
+
+struct State {
+ tx: Option<mpsc::Sender<(String, Option<String>)>>,
+ peers: HashMap<String, Peer>,
+}
+
+#[derive(Clone)]
+pub struct Server {
+ state: Arc<Mutex<State>>,
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum ServerError {
+ #[error("error during handshake {0}")]
+ Handshake(#[from] async_tungstenite::tungstenite::Error),
+}
+
+impl Server {
+ #[instrument(level = "debug", skip(factory))]
+ pub fn spawn<
+ I: for<'a> Deserialize<'a>,
+ O: Serialize + std::fmt::Debug,
+ Factory: FnOnce(Pin<Box<dyn Stream<Item = (String, Option<I>)> + Send>>) -> St,
+ St: Stream<Item = (String, O)>,
+ >(
+ factory: Factory,
+ ) -> Self
+ where
+ O: Serialize + std::fmt::Debug,
+ St: Send + Unpin + 'static,
+ {
+ let (tx, rx) = mpsc::channel::<(String, Option<String>)>(1000);
+ let mut handler = factory(Box::pin(rx.filter_map(|(peer_id, msg)| async move {
+ if let Some(msg) = msg {
+ match serde_json::from_str::<I>(&msg) {
+ Ok(msg) => Some((peer_id, Some(msg))),
+ Err(err) => {
+ warn!("Failed to parse incoming message: {} ({})", err, msg);
+ None
+ }
+ }
+ } else {
+ Some((peer_id, None))
+ }
+ })));
+
+ let state = Arc::new(Mutex::new(State {
+ tx: Some(tx),
+ peers: HashMap::new(),
+ }));
+
+ let state_clone = state.clone();
+ let _ = task::spawn(async move {
+ while let Some((peer_id, msg)) = handler.next().await {
+ match serde_json::to_string(&msg) {
+ Ok(msg) => {
+ if let Some(peer) = state_clone.lock().unwrap().peers.get_mut(&peer_id) {
+ let mut sender = peer.sender.clone();
+ task::spawn(async move {
+ let _ = sender.send(msg).await;
+ });
+ }
+ }
+ Err(err) => {
+ warn!("Failed to serialize outgoing message: {}", err);
+ }
+ }
+ }
+ });
+
+ Self { state }
+ }
+
+ #[instrument(level = "debug", skip(state))]
+ fn remove_peer(state: Arc<Mutex<State>>, peer_id: &str) {
+ if let Some(mut peer) = state.lock().unwrap().peers.remove(peer_id) {
+ let peer_id = peer_id.to_string();
+ task::spawn(async move {
+ peer.sender.close_channel();
+ if let Err(err) = peer.send_task_handle.await {
+ trace!(peer_id = %peer_id, "Error while joining send task: {}", err);
+ }
+ peer.receive_task_handle.await;
+ });
+ }
+ }
+
+ #[instrument(level = "debug", skip(self, stream))]
+ pub async fn accept_async<S: 'static>(&mut self, stream: S) -> Result<String, ServerError>
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send,
+ {
+ let ws = match async_tungstenite::accept_async(stream).await {
+ Ok(ws) => ws,
+ Err(err) => {
+ warn!("Error during the websocket handshake: {}", err);
+ return Err(ServerError::Handshake(err));
+ }
+ };
+
+ let this_id = uuid::Uuid::new_v4().to_string();
+ info!(this_id = %this_id, "New WebSocket connection");
+
+ // 1000 is completely arbitrary, we simply don't want infinite piling
+ // up of messages as with unbounded
+ let (websocket_sender, mut websocket_receiver) = mpsc::channel::<String>(1000);
+
+ let this_id_clone = this_id.clone();
+ let (mut ws_sink, mut ws_stream) = ws.split();
+ let send_task_handle = task::spawn(async move {
+ loop {
+ match async_std::future::timeout(
+ std::time::Duration::from_secs(30),
+ websocket_receiver.next(),
+ )
+ .await
+ {
+ Ok(Some(msg)) => {
+ trace!(this_id = %this_id_clone, "sending {}", msg);
+ ws_sink.send(WsMessage::Text(msg)).await?;
+ }
+ Ok(None) => {
+ break;
+ }
+ Err(_) => {
+ trace!(this_id = %this_id_clone, "timeout, sending ping");
+ ws_sink.send(WsMessage::Ping(vec![])).await?;
+ }
+ }
+ }
+
+ ws_sink.send(WsMessage::Close(None)).await?;
+ ws_sink.close().await?;
+
+ Ok::<(), Error>(())
+ });
+
+ let mut tx = self.state.lock().unwrap().tx.clone();
+ let this_id_clone = this_id.clone();
+ let state_clone = self.state.clone();
+ let receive_task_handle = task::spawn(async move {
+ if let Some(tx) = tx.as_mut() {
+ if let Err(err) = tx
+ .send((
+ this_id_clone.clone(),
+ Some(
+ serde_json::json!({
+ "type": "newPeer",
+ })
+ .to_string(),
+ ),
+ ))
+ .await
+ {
+ warn!(this = %this_id_clone, "Error handling message: {:?}", err);
+ }
+ }
+ while let Some(msg) = ws_stream.next().await {
+ info!("Received message {msg:?}");
+ match msg {
+ Ok(WsMessage::Text(msg)) => {
+ if let Some(tx) = tx.as_mut() {
+ if let Err(err) = tx.send((this_id_clone.clone(), Some(msg))).await {
+ warn!(this = %this_id_clone, "Error handling message: {:?}", err);
+ }
+ }
+ }
+ Ok(WsMessage::Close(reason)) => {
+ info!(this_id = %this_id_clone, "connection closed: {:?}", reason);
+ break;
+ }
+ Ok(WsMessage::Pong(_)) => {
+ continue;
+ }
+ Ok(_) => warn!(this_id = %this_id_clone, "Unsupported message type"),
+ Err(err) => {
+ warn!(this_id = %this_id_clone, "recv error: {}", err);
+ break;
+ }
+ }
+ }
+
+ if let Some(tx) = tx.as_mut() {
+ let _ = tx.send((this_id_clone.clone(), None)).await;
+ }
+
+ Self::remove_peer(state_clone, &this_id_clone);
+ });
+
+ self.state.lock().unwrap().peers.insert(
+ this_id.clone(),
+ Peer {
+ receive_task_handle,
+ send_task_handle,
+ sender: websocket_sender,
+ },
+ );
+
+ Ok(this_id)
+ }
+}
diff --git a/net/webrtc/www/index.html b/net/webrtc/www/index.html
new file mode 100644
index 00000000..e533c4ff
--- /dev/null
+++ b/net/webrtc/www/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+ vim: set sts=2 sw=2 et :
+
+
+ Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
+ with a GStreamer app. Runs only in passive mode, i.e., responds to offers
+ with answers, exchanges ICE candidates, and streams.
+
+ Author: Nirbheek Chauhan <nirbheek@centricular.com>
+-->
+<html>
+ <head>
+ <style>
+ .error { color: red; }
+ </style>
+ <link rel="stylesheet" type="text/css" href="theme.css">
+ <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
+ <script src="keyboard.js"></script>
+ <script src="input.js"></script>
+ <script src="webrtc.js"></script>
+ <script>window.onload = setup;</script>
+ </head>
+
+ <body>
+ <div class="holygrail-body">
+ <div class="content">
+ <div id="sessions">
+ </div>
+ <div id="image-holder">
+ <img id="image"></img>
+ </div>
+ </div>
+ <ul class="nav" id="camera-list">
+ </ul>
+ </div>
+ </body>
+</html>
diff --git a/net/webrtc/www/input.js b/net/webrtc/www/input.js
new file mode 100644
index 00000000..805f3557
--- /dev/null
+++ b/net/webrtc/www/input.js
@@ -0,0 +1,482 @@
+/**
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*global GamepadManager*/
+/*eslint no-unused-vars: ["error", { "vars": "local" }]*/
+
+
+class Input {
+ /**
+ * Input handling for WebRTC web app
+ *
+ * @constructor
+ * @param {Element} [element]
+ * Video element to attach events to
+ * @param {function} [send]
+ * Function used to send input events to server.
+ */
+ constructor(element, send) {
+ /**
+ * @type {Element}
+ */
+ this.element = element;
+
+ /**
+ * @type {function}
+ */
+ this.send = send;
+
+ /**
+ * @type {boolean}
+ */
+ this.mouseRelative = false;
+
+ /**
+ * @type {Object}
+ */
+ this.m = null;
+
+ /**
+ * @type {Keyboard}
+ */
+ this.keyboard = null;
+
+ /**
+ * @type {GamepadManager}
+ */
+ this.gamepadManager = null;
+
+ /**
+ * @type {Integer}
+ */
+ this.x = 0;
+
+ /**
+ * @type {Integer}
+ */
+ this.y = 0;
+
+ /**
+ * @type {Integer}
+ */
+ this.lastTouch = 0;
+
+ /**
+ * @type {function}
+ */
+ this.ongamepadconnected = null;
+
+ /**
+ * @type {function}
+ */
+ this.ongamepaddisconneceted = null;
+
+ /**
+ * List of attached listeners, record keeping used to detach all.
+ * @type {Array}
+ */
+ this.listeners = [];
+
+ /**
+ * @type {function}
+ */
+ this.onresizeend = null;
+
+ // internal variables used by resize start/end functions.
+ this._rtime = null;
+ this._rtimeout = false;
+ this._rdelta = 200;
+ }
+
+ /**
+ * Handles mouse button and motion events and sends them to WebRTC app.
+ * @param {MouseEvent} event
+ */
+ _mouseButtonMovement(event) {
+ const down = (event.type === 'mousedown' ? 1 : 0);
+ var data = {};
+
+ if (event.type === 'mousemove' && !this.m) return;
+
+ if (!document.pointerLockElement) {
+ if (this.mouseRelative)
+ event.target.requestPointerLock();
+ }
+
+ // Hotkey to enable pointer lock, CTRL-SHIFT-LeftButton
+ if (down && event.button === 0 && event.ctrlKey && event.shiftKey) {
+ event.target.requestPointerLock();
+ return;
+ }
+
+ if (document.pointerLockElement) {
+ // FIXME - mark as relative!
+ console.warn("FIXME: Make event relative!")
+ this.x = event.movementX;
+ this.y = event.movementY;
+ } else if (event.type === 'mousemove') {
+ this.x = this._clientToServerX(event.clientX);
+ this.y = this._clientToServerY(event.clientY);
+ data["event"] = "MouseMove"
+ }
+
+ if (event.type === 'mousedown') {
+ data["event"] = "MouseButtonPress";
+ } else if (event.type === 'mouseup') {
+ data["event"] = "MouseButtonRelease";
+ }
+
+ if (event.type === 'mousedown' || event.type === 'mouseup') {
+ data["button"] = event.button + 1;
+ }
+
+ data["x"] = this.x;
+ data["y"] = this.y;
+ data["modifier_state"] = this._modifierState(event);
+
+ this.send(data);
+ }
+
+ /**
+ * Handles touch events and sends them to WebRTC app.
+ * @param {TouchEvent} event
+ */
+ _touch(event) {
+ var mod_state = this._modifierState(event);
+
+ // Use TouchUp for cancelled touch points
+ if (event.type === 'touchcancel') {
+ let data = {};
+
+ data["event"] = "TouchUp";
+ data["identifier"] = event.changedTouches[0].identifier;
+ data["x"] = this._clientToServerX(event.changedTouches[0].clientX);
+ data["y"] = this._clientToServerY(event.changedTouches[0].clientY);
+ data["modifier_state"] = mod_state;
+
+ this.send(data);
+ return;
+ }
+
+ if (event.type === 'touchstart') {
+ var event_name = "TouchDown";
+ } else if (event.type === 'touchmove') {
+ var event_name = "TouchMotion";
+ } else if (event.type === 'touchend') {
+ var event_name = "TouchUp";
+ }
+
+ for (let touch of event.changedTouches) {
+ let data = {};
+
+ data["event"] = event_name;
+ data["identifier"] = touch.identifier;
+ data["x"] = this._clientToServerX(touch.clientX);
+ data["y"] = this._clientToServerY(touch.clientY);
+ data["modifier_state"] = mod_state;
+
+ if (event.type !== 'touchend') {
+ if ('force' in touch) {
+ data["pressure"] = touch.force;
+ } else {
+ data["pressure"] = NaN;
+ }
+ }
+
+ this.send(data);
+ }
+
+ if (event.timeStamp > this.lastTouch) {
+ let data = {};
+
+ data["event"] = "TouchFrame";
+ data["modifier_state"] = mod_state;
+
+ this.send(data);
+ this.lastTouch = event.timeStamp;
+ }
+
+ event.preventDefault();
+ }
+
+ /**
+ * Handles mouse wheel events and sends them to WebRTC app.
+ * @param {MouseEvent} event
+ */
+ _wheel(event) {
+ let data = {
+ "event": "MouseScroll",
+ "x": this.x,
+ "y": this.y,
+ "delta_x": -event.deltaX,
+ "delta_y": -event.deltaY,
+ "modifier_state": this._modifierState(event),
+ };
+
+ this.send(data);
+
+ event.preventDefault();
+ }
+
+ /**
+ * Captures mouse context menu (right-click) event and prevents event propagation.
+ * @param {MouseEvent} event
+ */
+ _contextMenu(event) {
+ event.preventDefault();
+ }
+
+ /**
+ * Sends WebRTC app command to hide the remote pointer when exiting pointer lock.
+ */
+ _exitPointerLock() {
+ document.exitPointerLock();
+ }
+
+ /**
+ * constructs the string representation for the active modifiers on the event
+ */
+ _modifierState(event) {
+ let masks = []
+ if (event.altKey) masks.push("alt-mask");
+ if (event.ctrlKey) masks.push("control-mask");
+ if (event.metaKey) masks.push("meta-mask");
+ if (event.shiftKey) masks.push("shift-mask");
+ return masks.join('+')
+ }
+
+ /**
+ * Captures display and video dimensions required for computing mouse pointer position.
+ * This should be fired whenever the window size changes.
+ */
+ _windowMath() {
+ const windowW = this.element.offsetWidth;
+ const windowH = this.element.offsetHeight;
+ const frameW = this.element.videoWidth;
+ const frameH = this.element.videoHeight;
+
+ const multi = Math.min(windowW / frameW, windowH / frameH);
+ const vpWidth = frameW * multi;
+ const vpHeight = (frameH * multi);
+
+ var elem = this.element;
+ var offsetLeft = 0;
+ var offsetTop = 0;
+ do {
+ if (!isNaN(elem.offsetLeft)) {
+ offsetLeft += elem.offsetLeft;
+ }
+
+ if (!isNaN(elem.offsetTop)) {
+ offsetTop += elem.offsetTop;
+ }
+ } while (elem = elem.offsetParent);
+
+ this.m = {
+ mouseMultiX: frameW / vpWidth,
+ mouseMultiY: frameH / vpHeight,
+ mouseOffsetX: Math.max((windowW - vpWidth) / 2.0, 0),
+ mouseOffsetY: Math.max((windowH - vpHeight) / 2.0, 0),
+ offsetLeft: offsetLeft,
+ offsetTop: offsetTop,
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ frameW,
+ frameH,
+ };
+ }
+
+ /**
+ * Translates pointer position X based on current window math.
+ * @param {Integer} clientX
+ */
+ _clientToServerX(clientX) {
+ var serverX = Math.round((clientX - this.m.mouseOffsetX - this.m.offsetLeft + this.m.scrollX) * this.m.mouseMultiX);
+
+ if (serverX === this.m.frameW - 1) serverX = this.m.frameW;
+ if (serverX > this.m.frameW) serverX = this.m.frameW;
+ if (serverX < 0) serverX = 0;
+
+ return serverX;
+ }
+
+ /**
+ * Translates pointer position Y based on current window math.
+ * @param {Integer} clientY
+ */
+ _clientToServerY(clientY) {
+ let serverY = Math.round((clientY - this.m.mouseOffsetY - this.m.offsetTop + this.m.scrollY) * this.m.mouseMultiY);
+
+ if (serverY === this.m.frameH - 1) serverY = this.m.frameH;
+ if (serverY > this.m.frameH) serverY = this.m.frameH;
+ if (serverY < 0) serverY = 0;
+
+ return serverY;
+ }
+
+ /**
+ * When fullscreen is entered, request keyboard and pointer lock.
+ */
+ _onFullscreenChange() {
+ if (document.fullscreenElement !== null) {
+ // Enter fullscreen
+ this.requestKeyboardLock();
+ this.element.requestPointerLock();
+ }
+ // Reset local keyboard. When holding to exit full-screen the escape key can get stuck.
+ this.keyboard.reset();
+
+ // Reset stuck keys on server side.
+ // FIXME: How to implement resetting keyboard with the GstNavigation interface
+ // this.send("kr");
+ }
+
+ /**
+ * Called when window is being resized, used to detect when resize ends so new resolution can be sent.
+ */
+ _resizeStart() {
+ this._rtime = new Date();
+ if (this._rtimeout === false) {
+ this._rtimeout = true;
+ setTimeout(() => { this._resizeEnd() }, this._rdelta);
+ }
+ }
+
+ /**
+ * Called in setTimeout loop to detect if window is done being resized.
+ */
+ _resizeEnd() {
+ if (new Date() - this._rtime < this._rdelta) {
+ setTimeout(() => { this._resizeEnd() }, this._rdelta);
+ } else {
+ this._rtimeout = false;
+ if (this.onresizeend !== null) {
+ this.onresizeend();
+ }
+ }
+ }
+
+ /**
+ * Attaches input event handles to docuemnt, window and element.
+ */
+ attach() {
+ this.listeners.push(addListener(this.element, 'resize', this._windowMath, this));
+ this.listeners.push(addListener(this.element, 'wheel', this._wheel, this));
+ this.listeners.push(addListener(this.element, 'contextmenu', this._contextMenu, this));
+ this.listeners.push(addListener(this.element.parentElement, 'fullscreenchange', this._onFullscreenChange, this));
+ this.listeners.push(addListener(window, 'resize', this._windowMath, this));
+ this.listeners.push(addListener(window, 'resize', this._resizeStart, this));
+
+ if ('ontouchstart' in window) {
+ console.warning("FIXME: Enabling mouse pointer display for touch devices.");
+ } else {
+ this.listeners.push(addListener(this.element, 'mousemove', this._mouseButtonMovement, this));
+ this.listeners.push(addListener(this.element, 'mousedown', this._mouseButtonMovement, this));
+ this.listeners.push(addListener(this.element, 'mouseup', this._mouseButtonMovement, this));
+ }
+
+ this.listeners.push(addListener(this.element, 'touchstart', this._touch, this));
+ this.listeners.push(addListener(this.element, 'touchend', this._touch, this));
+ this.listeners.push(addListener(this.element, 'touchmove', this._touch, this));
+ this.listeners.push(addListener(this.element, 'touchcancel', this._touch, this));
+
+ // Adjust for scroll offset
+ this.listeners.push(addListener(window, 'scroll', () => {
+ this.m.scrollX = window.scrollX;
+ this.m.scrollY = window.scrollY;
+ }, this));
+
+ // Using guacamole keyboard because it has the keysym translations.
+ this.keyboard = new Keyboard(this.element);
+ this.keyboard.onkeydown = (keysym, state) => {
+ this.send({"event": "KeyPress", "key": keysym, "modifier_state": state});
+ };
+ this.keyboard.onkeyup = (keysym, state) => {
+ this.send({"event": "KeyRelease", "key": keysym, "modifier_state": state});
+ };
+
+ this._windowMath();
+ }
+
+ detach() {
+ removeListeners(this.listeners);
+ this._exitPointerLock();
+ if (this.keyboard) {
+ this.keyboard.onkeydown = null;
+ this.keyboard.onkeyup = null;
+ this.keyboard.reset();
+ delete this.keyboard;
+ // FIXME: How to implement resetting keyboard with the GstNavigation interface
+ // this.send("kr");
+ }
+ }
+
+ /**
+ * Request keyboard lock, must be in fullscreen mode to work.
+ */
+ requestKeyboardLock() {
+ // event codes: https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system
+ const keys = [
+ "AltLeft",
+ "AltRight",
+ "Tab",
+ "Escape",
+ "ContextMenu",
+ "MetaLeft",
+ "MetaRight"
+ ];
+ console.log("requesting keyboard lock");
+ navigator.keyboard.lock(keys).then(
+ () => {
+ console.log("keyboard lock success");
+ }
+ ).catch(
+ (e) => {
+ console.log("keyboard lock failed: ", e);
+ }
+ )
+ }
+
+ getWindowResolution() {
+ return [
+ parseInt(this.element.offsetWidth * window.devicePixelRatio),
+ parseInt(this.element.offsetHeight * window.devicePixelRatio)
+ ];
+ }
+}
+
+/**
+ * Helper function to keep track of attached event listeners.
+ * @param {Object} obj
+ * @param {string} name
+ * @param {function} func
+ * @param {Object} ctx
+ */
+function addListener(obj, name, func, ctx) {
+ const newFunc = ctx ? func.bind(ctx) : func;
+ obj.addEventListener(name, newFunc);
+
+ return [obj, name, newFunc];
+}
+
+/**
+ * Helper function to remove all attached event listeners.
+ * @param {Array} listeners
+ */
+function removeListeners(listeners) {
+ for (const listener of listeners)
+ listener[0].removeEventListener(listener[1], listener[2]);
+}
diff --git a/net/webrtc/www/keyboard.js b/net/webrtc/www/keyboard.js
new file mode 100644
index 00000000..4305cd1a
--- /dev/null
+++ b/net/webrtc/www/keyboard.js
@@ -0,0 +1,3302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Provides cross-browser and cross-keyboard keyboard for a specific element.
+ * Browser and keyboard layout variation is abstracted away, providing events
+ * which represent keys as their corresponding X11 keysym.
+ *
+ * @constructor
+ * @param {Element} element The Element to use to provide keyboard events.
+ */
+Keyboard = function(element) {
+
+ /**
+ * Reference to this Keyboard.
+ * @private
+ */
+ var guac_keyboard = this;
+
+ /**
+ * Fired whenever the user presses a key with the element associated
+ * with this Keyboard in focus.
+ *
+ * @event
+ * @param {Number} keysym The keysym of the key being pressed.
+ * @param {String} modifier_state The string representation of
+ * all active modifiers.
+ * @return {Boolean} true if the key event should be allowed through to the
+ * browser, false otherwise.
+ */
+ this.onkeydown = null;
+
+ /**
+ * Fired whenever the user releases a key with the element associated
+ * with this Keyboard in focus.
+ *
+ * @event
+ * @param {Number} keysym The keysym of the key being released.
+ * @param {String} modifier_state The string representation of
+ * all active modifiers.
+ */
+ this.onkeyup = null;
+
+ /**
+ * A key event having a corresponding timestamp. This event is non-specific.
+ * Its subclasses should be used instead when recording specific key
+ * events.
+ *
+ * @private
+ * @constructor
+ */
+ var KeyEvent = function() {
+
+ /**
+ * Reference to this key event.
+ */
+ var key_event = this;
+
+ /**
+ * An arbitrary timestamp in milliseconds, indicating this event's
+ * position in time relative to other events.
+ *
+ * @type {Number}
+ */
+ this.timestamp = new Date().getTime();
+
+ /**
+ * Whether the default action of this key event should be prevented.
+ *
+ * @type {Boolean}
+ */
+ this.defaultPrevented = false;
+
+ /**
+ * The keysym of the key associated with this key event, as determined
+ * by a best-effort guess using available event properties and keyboard
+ * state.
+ *
+ * @type {Number}
+ */
+ this.keysym = null;
+
+ /**
+ * Whether the keysym value of this key event is known to be reliable.
+ * If false, the keysym may still be valid, but it's only a best guess,
+ * and future key events may be a better source of information.
+ *
+ * @type {Boolean}
+ */
+ this.reliable = false;
+
+ /**
+ * Returns the number of milliseconds elapsed since this event was
+ * received.
+ *
+ * @return {Number} The number of milliseconds elapsed since this
+ * event was received.
+ */
+ this.getAge = function() {
+ return new Date().getTime() - key_event.timestamp;
+ };
+
+ };
+
+ /**
+ * Information related to the pressing of a key, which need not be a key
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Keyboard.KeyEvent
+ * @param {Number} keyCode The JavaScript key code of the key pressed.
+ * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
+ * pressed, as defined at:
+ * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
+ * @param {String} key The standard name of the key pressed, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ * @param {Number} location The location on the keyboard corresponding to
+ * the key pressed, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ */
+ var KeydownEvent = function(keyCode, keyIdentifier, key, location) {
+
+ // We extend KeyEvent
+ KeyEvent.apply(this);
+
+ /**
+ * The JavaScript key code of the key pressed.
+ *
+ * @type {Number}
+ */
+ this.keyCode = keyCode;
+
+ /**
+ * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
+ * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
+ *
+ * @type {String}
+ */
+ this.keyIdentifier = keyIdentifier;
+
+ /**
+ * The standard name of the key pressed, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {String}
+ */
+ this.key = key;
+
+ /**
+ * The location on the keyboard corresponding to the key pressed, as
+ * defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {Number}
+ */
+ this.location = location;
+
+ // If key is known from keyCode or DOM3 alone, use that
+ this.keysym = keysym_from_key_identifier(key, location)
+ || keysym_from_keycode(keyCode, location);
+
+ // DOM3 and keyCode are reliable sources if the corresponding key is
+ // not a printable key
+ if (this.keysym && !isPrintable(this.keysym))
+ this.reliable = true;
+
+ // Use legacy keyIdentifier as a last resort, if it looks sane
+ if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier))
+ this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift);
+
+ // Determine whether default action for Alt+combinations must be prevented
+ var prevent_alt = !guac_keyboard.modifiers.ctrl
+ && !(navigator && navigator.platform && navigator.platform.match(/^mac/i));
+
+ // Determine whether default action for Ctrl+combinations must be prevented
+ var prevent_ctrl = !guac_keyboard.modifiers.alt;
+
+ // We must rely on the (potentially buggy) keyIdentifier if preventing
+ // the default action is important
+ if ((prevent_ctrl && guac_keyboard.modifiers.ctrl)
+ || (prevent_alt && guac_keyboard.modifiers.alt)
+ || guac_keyboard.modifiers.meta
+ || guac_keyboard.modifiers.hyper)
+ this.reliable = true;
+
+ // Record most recently known keysym by associated key code
+ recentKeysym[keyCode] = this.keysym;
+
+ };
+
+ KeydownEvent.prototype = new KeyEvent();
+
+ /**
+ * Information related to the pressing of a key, which MUST be
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Keyboard.KeyEvent
+ * @param {Number} charCode The Unicode codepoint of the character that
+ * would be typed by the key pressed.
+ */
+ var KeypressEvent = function(charCode) {
+
+ // We extend KeyEvent
+ KeyEvent.apply(this);
+
+ /**
+ * The Unicode codepoint of the character that would be typed by the
+ * key pressed.
+ *
+ * @type {Number}
+ */
+ this.charCode = charCode;
+
+ // Pull keysym from char code
+ this.keysym = keysym_from_charcode(charCode);
+
+ // Keypress is always reliable
+ this.reliable = true;
+
+ };
+
+ KeypressEvent.prototype = new KeyEvent();
+
+ /**
+ * Information related to the pressing of a key, which need not be a key
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Keyboard.KeyEvent
+ * @param {Number} keyCode The JavaScript key code of the key released.
+ * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
+ * released, as defined at:
+ * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
+ * @param {String} key The standard name of the key released, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ * @param {Number} location The location on the keyboard corresponding to
+ * the key released, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ */
+ var KeyupEvent = function(keyCode, keyIdentifier, key, location) {
+
+ // We extend KeyEvent
+ KeyEvent.apply(this);
+
+ /**
+ * The JavaScript key code of the key released.
+ *
+ * @type {Number}
+ */
+ this.keyCode = keyCode;
+
+ /**
+ * The legacy DOM3 "keyIdentifier" of the key released, as defined at:
+ * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
+ *
+ * @type {String}
+ */
+ this.keyIdentifier = keyIdentifier;
+
+ /**
+ * The standard name of the key released, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {String}
+ */
+ this.key = key;
+
+ /**
+ * The location on the keyboard corresponding to the key released, as
+ * defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {Number}
+ */
+ this.location = location;
+
+ // If key is known from keyCode or DOM3 alone, use that
+ this.keysym = recentKeysym[keyCode]
+ || keysym_from_keycode(keyCode, location)
+ || keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use
+
+ // Keyup is as reliable as it will ever be
+ this.reliable = true;
+
+ };
+
+ KeyupEvent.prototype = new KeyEvent();
+
+ /**
+ * An array of recorded events, which can be instances of the private
+ * KeydownEvent, KeypressEvent, and KeyupEvent classes.
+ *
+ * @private
+ * @type {KeyEvent[]}
+ */
+ var eventLog = [];
+
+ /**
+ * Map of known JavaScript keycodes which do not map to typable characters
+ * to their X11 keysym equivalents.
+ * @private
+ */
+ var keycodeKeysyms = {
+ 8: [0xFF08], // backspace
+ 9: [0xFF09], // tab
+ 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5
+ 13: [0xFF0D], // enter
+ 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift
+ 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
+ 18: [0xFFE9, 0xFFE9, 0xFE03], // alt
+ 19: [0xFF13], // pause/break
+ 20: [0xFFE5], // caps lock
+ 27: [0xFF1B], // escape
+ 32: [0x0020], // space
+ 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9
+ 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3
+ 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1
+ 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7
+ 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4
+ 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8
+ 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
+ 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
+ 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
+ 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
+ 91: [0xFFEB], // left window key (hyper_l)
+ 92: [0xFF67], // right window key (menu key?)
+ 93: null, // select key
+ 96: [0xFFB0], // KP 0
+ 97: [0xFFB1], // KP 1
+ 98: [0xFFB2], // KP 2
+ 99: [0xFFB3], // KP 3
+ 100: [0xFFB4], // KP 4
+ 101: [0xFFB5], // KP 5
+ 102: [0xFFB6], // KP 6
+ 103: [0xFFB7], // KP 7
+ 104: [0xFFB8], // KP 8
+ 105: [0xFFB9], // KP 9
+ 106: [0xFFAA], // KP multiply
+ 107: [0xFFAB], // KP add
+ 109: [0xFFAD], // KP subtract
+ 110: [0xFFAE], // KP decimal
+ 111: [0xFFAF], // KP divide
+ 112: [0xFFBE], // f1
+ 113: [0xFFBF], // f2
+ 114: [0xFFC0], // f3
+ 115: [0xFFC1], // f4
+ 116: [0xFFC2], // f5
+ 117: [0xFFC3], // f6
+ 118: [0xFFC4], // f7
+ 119: [0xFFC5], // f8
+ 120: [0xFFC6], // f9
+ 121: [0xFFC7], // f10
+ 122: [0xFFC8], // f11
+ 123: [0xFFC9], // f12
+ 144: [0xFF7F], // num lock
+ 145: [0xFF14], // scroll lock
+ 225: [0xFE03] // altgraph (iso_level3_shift)
+ };
+
+ /**
+ * Map of known JavaScript keyidentifiers which do not map to typable
+ * characters to their unshifted X11 keysym equivalents.
+ * @private
+ */
+ var keyidentifier_keysym = {
+ "Again": [0xFF66],
+ "AllCandidates": [0xFF3D],
+ "Alphanumeric": [0xFF30],
+ "Alt": [0xFFE9, 0xFFE9, 0xFE03],
+ "Attn": [0xFD0E],
+ "AltGraph": [0xFE03],
+ "ArrowDown": [0xFF54],
+ "ArrowLeft": [0xFF51],
+ "ArrowRight": [0xFF53],
+ "ArrowUp": [0xFF52],
+ "Backspace": [0xFF08],
+ "CapsLock": [0xFFE5],
+ "Cancel": [0xFF69],
+ "Clear": [0xFF0B],
+ "Convert": [0xFF21],
+ "Copy": [0xFD15],
+ "Crsel": [0xFD1C],
+ "CrSel": [0xFD1C],
+ "CodeInput": [0xFF37],
+ "Compose": [0xFF20],
+ "Control": [0xFFE3, 0xFFE3, 0xFFE4],
+ "ContextMenu": [0xFF67],
+ "DeadGrave": [0xFE50],
+ "DeadAcute": [0xFE51],
+ "DeadCircumflex": [0xFE52],
+ "DeadTilde": [0xFE53],
+ "DeadMacron": [0xFE54],
+ "DeadBreve": [0xFE55],
+ "DeadAboveDot": [0xFE56],
+ "DeadUmlaut": [0xFE57],
+ "DeadAboveRing": [0xFE58],
+ "DeadDoubleacute": [0xFE59],
+ "DeadCaron": [0xFE5A],
+ "DeadCedilla": [0xFE5B],
+ "DeadOgonek": [0xFE5C],
+ "DeadIota": [0xFE5D],
+ "DeadVoicedSound": [0xFE5E],
+ "DeadSemivoicedSound": [0xFE5F],
+ "Delete": [0xFFFF],
+ "Down": [0xFF54],
+ "End": [0xFF57],
+ "Enter": [0xFF0D],
+ "EraseEof": [0xFD06],
+ "Escape": [0xFF1B],
+ "Execute": [0xFF62],
+ "Exsel": [0xFD1D],
+ "ExSel": [0xFD1D],
+ "F1": [0xFFBE],
+ "F2": [0xFFBF],
+ "F3": [0xFFC0],
+ "F4": [0xFFC1],
+ "F5": [0xFFC2],
+ "F6": [0xFFC3],
+ "F7": [0xFFC4],
+ "F8": [0xFFC5],
+ "F9": [0xFFC6],
+ "F10": [0xFFC7],
+ "F11": [0xFFC8],
+ "F12": [0xFFC9],
+ "F13": [0xFFCA],
+ "F14": [0xFFCB],
+ "F15": [0xFFCC],
+ "F16": [0xFFCD],
+ "F17": [0xFFCE],
+ "F18": [0xFFCF],
+ "F19": [0xFFD0],
+ "F20": [0xFFD1],
+ "F21": [0xFFD2],
+ "F22": [0xFFD3],
+ "F23": [0xFFD4],
+ "F24": [0xFFD5],
+ "Find": [0xFF68],
+ "GroupFirst": [0xFE0C],
+ "GroupLast": [0xFE0E],
+ "GroupNext": [0xFE08],
+ "GroupPrevious": [0xFE0A],
+ "FullWidth": null,
+ "HalfWidth": null,
+ "HangulMode": [0xFF31],
+ "Hankaku": [0xFF29],
+ "HanjaMode": [0xFF34],
+ "Help": [0xFF6A],
+ "Hiragana": [0xFF25],
+ "HiraganaKatakana": [0xFF27],
+ "Home": [0xFF50],
+ "Hyper": [0xFFED, 0xFFED, 0xFFEE],
+ "Insert": [0xFF63],
+ "JapaneseHiragana": [0xFF25],
+ "JapaneseKatakana": [0xFF26],
+ "JapaneseRomaji": [0xFF24],
+ "JunjaMode": [0xFF38],
+ "KanaMode": [0xFF2D],
+ "KanjiMode": [0xFF21],
+ "Katakana": [0xFF26],
+ "Left": [0xFF51],
+ "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
+ "ModeChange": [0xFF7E],
+ "NumLock": [0xFF7F],
+ "PageDown": [0xFF56],
+ "PageUp": [0xFF55],
+ "Pause": [0xFF13],
+ "Play": [0xFD16],
+ "PreviousCandidate": [0xFF3E],
+ "PrintScreen": [0xFD1D],
+ "Redo": [0xFF66],
+ "Right": [0xFF53],
+ "RomanCharacters": null,
+ "Scroll": [0xFF14],
+ "Select": [0xFF60],
+ "Separator": [0xFFAC],
+ "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
+ "SingleCandidate": [0xFF3C],
+ "Super": [0xFFEB, 0xFFEB, 0xFFEC],
+ "Tab": [0xFF09],
+ "Up": [0xFF52],
+ "Undo": [0xFF65],
+ "Win": [0xFFEB],
+ "Zenkaku": [0xFF28],
+ "ZenkakuHankaku": [0xFF2A]
+ };
+
+ const keysym_to_string = {
+ 0xFFFFFF: "VoidSymbol",
+ 0xFF08: "BackSpace",
+ 0xFF09: "Tab",
+ 0xFF0A: "Linefeed",
+ 0xFF0B: "Clear",
+ 0xFF0D: "Return",
+ 0xFF13: "Pause",
+ 0xFF14: "Scroll_Lock",
+ 0xFF15: "Sys_Req",
+ 0xFF1B: "Escape",
+ 0xFFFF: "Delete",
+ 0xFF20: "Multi_key",
+ 0xFF37: "Codeinput",
+ 0xFF3C: "SingleCandidate",
+ 0xFF3D: "MultipleCandidate",
+ 0xFF3E: "PreviousCandidate",
+ 0xFF21: "Kanji",
+ 0xFF22: "Muhenkan",
+ 0xFF23: "Henkan_Mode",
+ 0xFF23: "Henkan",
+ 0xFF24: "Romaji",
+ 0xFF25: "Hiragana",
+ 0xFF26: "Katakana",
+ 0xFF27: "Hiragana_Katakana",
+ 0xFF28: "Zenkaku",
+ 0xFF29: "Hankaku",
+ 0xFF2A: "Zenkaku_Hankaku",
+ 0xFF2B: "Touroku",
+ 0xFF2C: "Massyo",
+ 0xFF2D: "Kana_Lock",
+ 0xFF2E: "Kana_Shift",
+ 0xFF2F: "Eisu_Shift",
+ 0xFF30: "Eisu_toggle",
+ 0xFF37: "Kanji_Bangou",
+ 0xFF3D: "Zen_Koho",
+ 0xFF3E: "Mae_Koho",
+ 0xFF50: "Home",
+ 0xFF51: "Left",
+ 0xFF52: "Up",
+ 0xFF53: "Right",
+ 0xFF54: "Down",
+ 0xFF55: "Prior",
+ 0xFF55: "Page_Up",
+ 0xFF56: "Next",
+ 0xFF56: "Page_Down",
+ 0xFF57: "End",
+ 0xFF58: "Begin",
+ 0xFF60: "Select",
+ 0xFF61: "Print",
+ 0xFF62: "Execute",
+ 0xFF63: "Insert",
+ 0xFF65: "Undo",
+ 0xFF66: "Redo",
+ 0xFF67: "Menu",
+ 0xFF68: "Find",
+ 0xFF69: "Cancel",
+ 0xFF6A: "Help",
+ 0xFF6B: "Break",
+ 0xFF7E: "Mode_switch",
+ 0xFF7E: "script_switch",
+ 0xFF7F: "Num_Lock",
+ 0xFF80: "KP_Space",
+ 0xFF89: "KP_Tab",
+ 0xFF8D: "KP_Enter",
+ 0xFF91: "KP_F1",
+ 0xFF92: "KP_F2",
+ 0xFF93: "KP_F3",
+ 0xFF94: "KP_F4",
+ 0xFF95: "KP_Home",
+ 0xFF96: "KP_Left",
+ 0xFF97: "KP_Up",
+ 0xFF98: "KP_Right",
+ 0xFF99: "KP_Down",
+ 0xFF9A: "KP_Prior",
+ 0xFF9A: "KP_Page_Up",
+ 0xFF9B: "KP_Next",
+ 0xFF9B: "KP_Page_Down",
+ 0xFF9C: "KP_End",
+ 0xFF9D: "KP_Begin",
+ 0xFF9E: "KP_Insert",
+ 0xFF9F: "KP_Delete",
+ 0xFFBD: "KP_Equal",
+ 0xFFAA: "KP_Multiply",
+ 0xFFAB: "KP_Add",
+ 0xFFAC: "KP_Separator",
+ 0xFFAD: "KP_Subtract",
+ 0xFFAE: "KP_Decimal",
+ 0xFFAF: "KP_Divide",
+ 0xFFB0: "KP_0",
+ 0xFFB1: "KP_1",
+ 0xFFB2: "KP_2",
+ 0xFFB3: "KP_3",
+ 0xFFB4: "KP_4",
+ 0xFFB5: "KP_5",
+ 0xFFB6: "KP_6",
+ 0xFFB7: "KP_7",
+ 0xFFB8: "KP_8",
+ 0xFFB9: "KP_9",
+ 0xFFBE: "F1",
+ 0xFFBF: "F2",
+ 0xFFC0: "F3",
+ 0xFFC1: "F4",
+ 0xFFC2: "F5",
+ 0xFFC3: "F6",
+ 0xFFC4: "F7",
+ 0xFFC5: "F8",
+ 0xFFC6: "F9",
+ 0xFFC7: "F10",
+ 0xFFC8: "F11",
+ 0xFFC8: "L1",
+ 0xFFC9: "F12",
+ 0xFFC9: "L2",
+ 0xFFCA: "F13",
+ 0xFFCA: "L3",
+ 0xFFCB: "F14",
+ 0xFFCB: "L4",
+ 0xFFCC: "F15",
+ 0xFFCC: "L5",
+ 0xFFCD: "F16",
+ 0xFFCD: "L6",
+ 0xFFCE: "F17",
+ 0xFFCE: "L7",
+ 0xFFCF: "F18",
+ 0xFFCF: "L8",
+ 0xFFD0: "F19",
+ 0xFFD0: "L9",
+ 0xFFD1: "F20",
+ 0xFFD1: "L10",
+ 0xFFD2: "F21",
+ 0xFFD2: "R1",
+ 0xFFD3: "F22",
+ 0xFFD3: "R2",
+ 0xFFD4: "F23",
+ 0xFFD4: "R3",
+ 0xFFD5: "F24",
+ 0xFFD5: "R4",
+ 0xFFD6: "F25",
+ 0xFFD6: "R5",
+ 0xFFD7: "F26",
+ 0xFFD7: "R6",
+ 0xFFD8: "F27",
+ 0xFFD8: "R7",
+ 0xFFD9: "F28",
+ 0xFFD9: "R8",
+ 0xFFDA: "F29",
+ 0xFFDA: "R9",
+ 0xFFDB: "F30",
+ 0xFFDB: "R10",
+ 0xFFDC: "F31",
+ 0xFFDC: "R11",
+ 0xFFDD: "F32",
+ 0xFFDD: "R12",
+ 0xFFDE: "F33",
+ 0xFFDE: "R13",
+ 0xFFDF: "F34",
+ 0xFFDF: "R14",
+ 0xFFE0: "F35",
+ 0xFFE0: "R15",
+ 0xFFE1: "Shift_L",
+ 0xFFE2: "Shift_R",
+ 0xFFE3: "Control_L",
+ 0xFFE4: "Control_R",
+ 0xFFE5: "Caps_Lock",
+ 0xFFE6: "Shift_Lock",
+ 0xFFE7: "Meta_L",
+ 0xFFE8: "Meta_R",
+ 0xFFE9: "Alt_L",
+ 0xFFEA: "Alt_R",
+ 0xFFEB: "Super_L",
+ 0xFFEC: "Super_R",
+ 0xFFED: "Hyper_L",
+ 0xFFEE: "Hyper_R",
+ 0xFE01: "ISO_Lock",
+ 0xFE02: "ISO_Level2_Latch",
+ 0xFE03: "ISO_Level3_Shift",
+ 0xFE04: "ISO_Level3_Latch",
+ 0xFE05: "ISO_Level3_Lock",
+ 0xFE11: "ISO_Level5_Shift",
+ 0xFE12: "ISO_Level5_Latch",
+ 0xFE13: "ISO_Level5_Lock",
+ 0xFF7E: "ISO_Group_Shift",
+ 0xFE06: "ISO_Group_Latch",
+ 0xFE07: "ISO_Group_Lock",
+ 0xFE08: "ISO_Next_Group",
+ 0xFE09: "ISO_Next_Group_Lock",
+ 0xFE0A: "ISO_Prev_Group",
+ 0xFE0B: "ISO_Prev_Group_Lock",
+ 0xFE0C: "ISO_First_Group",
+ 0xFE0D: "ISO_First_Group_Lock",
+ 0xFE0E: "ISO_Last_Group",
+ 0xFE0F: "ISO_Last_Group_Lock",
+ 0xFE20: "ISO_Left_Tab",
+ 0xFE21: "ISO_Move_Line_Up",
+ 0xFE22: "ISO_Move_Line_Down",
+ 0xFE23: "ISO_Partial_Line_Up",
+ 0xFE24: "ISO_Partial_Line_Down",
+ 0xFE25: "ISO_Partial_Space_Left",
+ 0xFE26: "ISO_Partial_Space_Right",
+ 0xFE27: "ISO_Set_Margin_Left",
+ 0xFE28: "ISO_Set_Margin_Right",
+ 0xFE29: "ISO_Release_Margin_Left",
+ 0xFE2A: "ISO_Release_Margin_Right",
+ 0xFE2B: "ISO_Release_Both_Margins",
+ 0xFE2C: "ISO_Fast_Cursor_Left",
+ 0xFE2D: "ISO_Fast_Cursor_Right",
+ 0xFE2E: "ISO_Fast_Cursor_Up",
+ 0xFE2F: "ISO_Fast_Cursor_Down",
+ 0xFE30: "ISO_Continuous_Underline",
+ 0xFE31: "ISO_Discontinuous_Underline",
+ 0xFE32: "ISO_Emphasize",
+ 0xFE33: "ISO_Center_Object",
+ 0xFE34: "ISO_Enter",
+ 0xFE50: "dead_grave",
+ 0xFE51: "dead_acute",
+ 0xFE52: "dead_circumflex",
+ 0xFE53: "dead_tilde",
+ 0xFE53: "dead_perispomeni",
+ 0xFE54: "dead_macron",
+ 0xFE55: "dead_breve",
+ 0xFE56: "dead_abovedot",
+ 0xFE57: "dead_diaeresis",
+ 0xFE58: "dead_abovering",
+ 0xFE59: "dead_doubleacute",
+ 0xFE5A: "dead_caron",
+ 0xFE5B: "dead_cedilla",
+ 0xFE5C: "dead_ogonek",
+ 0xFE5D: "dead_iota",
+ 0xFE5E: "dead_voiced_sound",
+ 0xFE5F: "dead_semivoiced_sound",
+ 0xFE60: "dead_belowdot",
+ 0xFE61: "dead_hook",
+ 0xFE62: "dead_horn",
+ 0xFE63: "dead_stroke",
+ 0xFE64: "dead_abovecomma",
+ 0xFE64: "dead_psili",
+ 0xFE65: "dead_abovereversedcomma",
+ 0xFE65: "dead_dasia",
+ 0xFE66: "dead_doublegrave",
+ 0xFE67: "dead_belowring",
+ 0xFE68: "dead_belowmacron",
+ 0xFE69: "dead_belowcircumflex",
+ 0xFE6A: "dead_belowtilde",
+ 0xFE6B: "dead_belowbreve",
+ 0xFE6C: "dead_belowdiaeresis",
+ 0xFE6D: "dead_invertedbreve",
+ 0xFE6E: "dead_belowcomma",
+ 0xFE6F: "dead_currency",
+ 0xFE90: "dead_lowline",
+ 0xFE91: "dead_aboveverticalline",
+ 0xFE92: "dead_belowverticalline",
+ 0xFE93: "dead_longsolidusoverlay",
+ 0xFE80: "dead_a",
+ 0xFE81: "dead_A",
+ 0xFE82: "dead_e",
+ 0xFE83: "dead_E",
+ 0xFE84: "dead_i",
+ 0xFE85: "dead_I",
+ 0xFE86: "dead_o",
+ 0xFE87: "dead_O",
+ 0xFE88: "dead_u",
+ 0xFE89: "dead_U",
+ 0xFE8A: "dead_small_schwa",
+ 0xFE8B: "dead_capital_schwa",
+ 0xFE8C: "dead_greek",
+ 0xFED0: "First_Virtual_Screen",
+ 0xFED1: "Prev_Virtual_Screen",
+ 0xFED2: "Next_Virtual_Screen",
+ 0xFED4: "Last_Virtual_Screen",
+ 0xFED5: "Terminate_Server",
+ 0xFE70: "AccessX_Enable",
+ 0xFE71: "AccessX_Feedback_Enable",
+ 0xFE72: "RepeatKeys_Enable",
+ 0xFE73: "SlowKeys_Enable",
+ 0xFE74: "BounceKeys_Enable",
+ 0xFE75: "StickyKeys_Enable",
+ 0xFE76: "MouseKeys_Enable",
+ 0xFE77: "MouseKeys_Accel_Enable",
+ 0xFE78: "Overlay1_Enable",
+ 0xFE79: "Overlay2_Enable",
+ 0xFE7A: "AudibleBell_Enable",
+ 0xFEE0: "Pointer_Left",
+ 0xFEE1: "Pointer_Right",
+ 0xFEE2: "Pointer_Up",
+ 0xFEE3: "Pointer_Down",
+ 0xFEE4: "Pointer_UpLeft",
+ 0xFEE5: "Pointer_UpRight",
+ 0xFEE6: "Pointer_DownLeft",
+ 0xFEE7: "Pointer_DownRight",
+ 0xFEE8: "Pointer_Button_Dflt",
+ 0xFEE9: "Pointer_Button1",
+ 0xFEEA: "Pointer_Button2",
+ 0xFEEB: "Pointer_Button3",
+ 0xFEEC: "Pointer_Button4",
+ 0xFEED: "Pointer_Button5",
+ 0xFEEE: "Pointer_DblClick_Dflt",
+ 0xFEEF: "Pointer_DblClick1",
+ 0xFEF0: "Pointer_DblClick2",
+ 0xFEF1: "Pointer_DblClick3",
+ 0xFEF2: "Pointer_DblClick4",
+ 0xFEF3: "Pointer_DblClick5",
+ 0xFEF4: "Pointer_Drag_Dflt",
+ 0xFEF5: "Pointer_Drag1",
+ 0xFEF6: "Pointer_Drag2",
+ 0xFEF7: "Pointer_Drag3",
+ 0xFEF8: "Pointer_Drag4",
+ 0xFEFD: "Pointer_Drag5",
+ 0xFEF9: "Pointer_EnableKeys",
+ 0xFEFA: "Pointer_Accelerate",
+ 0xFEFB: "Pointer_DfltBtnNext",
+ 0xFEFC: "Pointer_DfltBtnPrev",
+ 0xFEA0: "ch",
+ 0xFEA1: "Ch",
+ 0xFEA2: "CH",
+ 0xFEA3: "c_h",
+ 0xFEA4: "C_h",
+ 0xFEA5: "C_H",
+ 0xFD01: "3270_Duplicate",
+ 0xFD02: "3270_FieldMark",
+ 0xFD03: "3270_Right2",
+ 0xFD04: "3270_Left2",
+ 0xFD05: "3270_BackTab",
+ 0xFD06: "3270_EraseEOF",
+ 0xFD07: "3270_EraseInput",
+ 0xFD08: "3270_Reset",
+ 0xFD09: "3270_Quit",
+ 0xFD0A: "3270_PA1",
+ 0xFD0B: "3270_PA2",
+ 0xFD0C: "3270_PA3",
+ 0xFD0D: "3270_Test",
+ 0xFD0E: "3270_Attn",
+ 0xFD0F: "3270_CursorBlink",
+ 0xFD10: "3270_AltCursor",
+ 0xFD11: "3270_KeyClick",
+ 0xFD12: "3270_Jump",
+ 0xFD13: "3270_Ident",
+ 0xFD14: "3270_Rule",
+ 0xFD15: "3270_Copy",
+ 0xFD16: "3270_Play",
+ 0xFD17: "3270_Setup",
+ 0xFD18: "3270_Record",
+ 0xFD19: "3270_ChangeScreen",
+ 0xFD1A: "3270_DeleteWord",
+ 0xFD1B: "3270_ExSelect",
+ 0xFD1C: "3270_CursorSelect",
+ 0xFD1D: "3270_PrintScreen",
+ 0xFD1E: "3270_Enter",
+ 0x0020: "space",
+ 0x0021: "exclam",
+ 0x0022: "quotedbl",
+ 0x0023: "numbersign",
+ 0x0024: "dollar",
+ 0x0025: "percent",
+ 0x0026: "ampersand",
+ 0x0027: "apostrophe",
+ 0x0027: "quoteright",
+ 0x0028: "parenleft",
+ 0x0029: "parenright",
+ 0x002A: "asterisk",
+ 0x002B: "plus",
+ 0x002C: "comma",
+ 0x002D: "minus",
+ 0x002E: "period",
+ 0x002F: "slash",
+ 0x0030: "0",
+ 0x0031: "1",
+ 0x0032: "2",
+ 0x0033: "3",
+ 0x0034: "4",
+ 0x0035: "5",
+ 0x0036: "6",
+ 0x0037: "7",
+ 0x0038: "8",
+ 0x0039: "9",
+ 0x003A: "colon",
+ 0x003B: "semicolon",
+ 0x003C: "less",
+ 0x003D: "equal",
+ 0x003E: "greater",
+ 0x003F: "question",
+ 0x0040: "at",
+ 0x0041: "A",
+ 0x0042: "B",
+ 0x0043: "C",
+ 0x0044: "D",
+ 0x0045: "E",
+ 0x0046: "F",
+ 0x0047: "G",
+ 0x0048: "H",
+ 0x0049: "I",
+ 0x004A: "J",
+ 0x004B: "K",
+ 0x004C: "L",
+ 0x004D: "M",
+ 0x004E: "N",
+ 0x004F: "O",
+ 0x0050: "P",
+ 0x0051: "Q",
+ 0x0052: "R",
+ 0x0053: "S",
+ 0x0054: "T",
+ 0x0055: "U",
+ 0x0056: "V",
+ 0x0057: "W",
+ 0x0058: "X",
+ 0x0059: "Y",
+ 0x005A: "Z",
+ 0x005B: "bracketleft",
+ 0x005C: "backslash",
+ 0x005D: "bracketright",
+ 0x005E: "asciicircum",
+ 0x005F: "underscore",
+ 0x0060: "grave",
+ 0x0060: "quoteleft",
+ 0x0061: "a",
+ 0x0062: "b",
+ 0x0063: "c",
+ 0x0064: "d",
+ 0x0065: "e",
+ 0x0066: "f",
+ 0x0067: "g",
+ 0x0068: "h",
+ 0x0069: "i",
+ 0x006A: "j",
+ 0x006B: "k",
+ 0x006C: "l",
+ 0x006D: "m",
+ 0x006E: "n",
+ 0x006F: "o",
+ 0x0070: "p",
+ 0x0071: "q",
+ 0x0072: "r",
+ 0x0073: "s",
+ 0x0074: "t",
+ 0x0075: "u",
+ 0x0076: "v",
+ 0x0077: "w",
+ 0x0078: "x",
+ 0x0079: "y",
+ 0x007A: "z",
+ 0x007B: "braceleft",
+ 0x007C: "bar",
+ 0x007D: "braceright",
+ 0x007E: "asciitilde",
+ 0x00A0: "nobreakspace",
+ 0x00A1: "exclamdown",
+ 0x00A2: "cent",
+ 0x00A3: "sterling",
+ 0x00A4: "currency",
+ 0x00A5: "yen",
+ 0x00A6: "brokenbar",
+ 0x00A7: "section",
+ 0x00A8: "diaeresis",
+ 0x00A9: "copyright",
+ 0x00AA: "ordfeminine",
+ 0x00AB: "guillemotleft",
+ 0x00AC: "notsign",
+ 0x00AD: "hyphen",
+ 0x00AE: "registered",
+ 0x00AF: "macron",
+ 0x00B0: "degree",
+ 0x00B1: "plusminus",
+ 0x00B2: "twosuperior",
+ 0x00B3: "threesuperior",
+ 0x00B4: "acute",
+ 0x00B5: "mu",
+ 0x00B6: "paragraph",
+ 0x00B7: "periodcentered",
+ 0x00B8: "cedilla",
+ 0x00B9: "onesuperior",
+ 0x00BA: "masculine",
+ 0x00BB: "guillemotright",
+ 0x00BC: "onequarter",
+ 0x00BD: "onehalf",
+ 0x00BE: "threequarters",
+ 0x00BF: "questiondown",
+ 0x00C0: "Agrave",
+ 0x00C1: "Aacute",
+ 0x00C2: "Acircumflex",
+ 0x00C3: "Atilde",
+ 0x00C4: "Adiaeresis",
+ 0x00C5: "Aring",
+ 0x00C6: "AE",
+ 0x00C7: "Ccedilla",
+ 0x00C8: "Egrave",
+ 0x00C9: "Eacute",
+ 0x00CA: "Ecircumflex",
+ 0x00CB: "Ediaeresis",
+ 0x00CC: "Igrave",
+ 0x00CD: "Iacute",
+ 0x00CE: "Icircumflex",
+ 0x00CF: "Idiaeresis",
+ 0x00D0: "ETH",
+ 0x00D0: "Eth",
+ 0x00D1: "Ntilde",
+ 0x00D2: "Ograve",
+ 0x00D3: "Oacute",
+ 0x00D4: "Ocircumflex",
+ 0x00D5: "Otilde",
+ 0x00D6: "Odiaeresis",
+ 0x00D7: "multiply",
+ 0x00D8: "Oslash",
+ 0x00D8: "Ooblique",
+ 0x00D9: "Ugrave",
+ 0x00DA: "Uacute",
+ 0x00DB: "Ucircumflex",
+ 0x00DC: "Udiaeresis",
+ 0x00DD: "Yacute",
+ 0x00DE: "THORN",
+ 0x00DE: "Thorn",
+ 0x00DF: "ssharp",
+ 0x00E0: "agrave",
+ 0x00E1: "aacute",
+ 0x00E2: "acircumflex",
+ 0x00E3: "atilde",
+ 0x00E4: "adiaeresis",
+ 0x00E5: "aring",
+ 0x00E6: "ae",
+ 0x00E7: "ccedilla",
+ 0x00E8: "egrave",
+ 0x00E9: "eacute",
+ 0x00EA: "ecircumflex",
+ 0x00EB: "ediaeresis",
+ 0x00EC: "igrave",
+ 0x00ED: "iacute",
+ 0x00EE: "icircumflex",
+ 0x00EF: "idiaeresis",
+ 0x00F0: "eth",
+ 0x00F1: "ntilde",
+ 0x00F2: "ograve",
+ 0x00F3: "oacute",
+ 0x00F4: "ocircumflex",
+ 0x00F5: "otilde",
+ 0x00F6: "odiaeresis",
+ 0x00F7: "division",
+ 0x00F8: "oslash",
+ 0x00F8: "ooblique",
+ 0x00F9: "ugrave",
+ 0x00FA: "uacute",
+ 0x00FB: "ucircumflex",
+ 0x00FC: "udiaeresis",
+ 0x00FD: "yacute",
+ 0x00FE: "thorn",
+ 0x00FF: "ydiaeresis",
+ 0x01A1: "Aogonek",
+ 0x01A2: "breve",
+ 0x01A3: "Lstroke",
+ 0x01A5: "Lcaron",
+ 0x01A6: "Sacute",
+ 0x01A9: "Scaron",
+ 0x01AA: "Scedilla",
+ 0x01AB: "Tcaron",
+ 0x01AC: "Zacute",
+ 0x01AE: "Zcaron",
+ 0x01AF: "Zabovedot",
+ 0x01B1: "aogonek",
+ 0x01B2: "ogonek",
+ 0x01B3: "lstroke",
+ 0x01B5: "lcaron",
+ 0x01B6: "sacute",
+ 0x01B7: "caron",
+ 0x01B9: "scaron",
+ 0x01BA: "scedilla",
+ 0x01BB: "tcaron",
+ 0x01BC: "zacute",
+ 0x01BD: "doubleacute",
+ 0x01BE: "zcaron",
+ 0x01BF: "zabovedot",
+ 0x01C0: "Racute",
+ 0x01C3: "Abreve",
+ 0x01C5: "Lacute",
+ 0x01C6: "Cacute",
+ 0x01C8: "Ccaron",
+ 0x01CA: "Eogonek",
+ 0x01CC: "Ecaron",
+ 0x01CF: "Dcaron",
+ 0x01D0: "Dstroke",
+ 0x01D1: "Nacute",
+ 0x01D2: "Ncaron",
+ 0x01D5: "Odoubleacute",
+ 0x01D8: "Rcaron",
+ 0x01D9: "Uring",
+ 0x01DB: "Udoubleacute",
+ 0x01DE: "Tcedilla",
+ 0x01E0: "racute",
+ 0x01E3: "abreve",
+ 0x01E5: "lacute",
+ 0x01E6: "cacute",
+ 0x01E8: "ccaron",
+ 0x01EA: "eogonek",
+ 0x01EC: "ecaron",
+ 0x01EF: "dcaron",
+ 0x01F0: "dstroke",
+ 0x01F1: "nacute",
+ 0x01F2: "ncaron",
+ 0x01F5: "odoubleacute",
+ 0x01F8: "rcaron",
+ 0x01F9: "uring",
+ 0x01FB: "udoubleacute",
+ 0x01FE: "tcedilla",
+ 0x01FF: "abovedot",
+ 0x02A1: "Hstroke",
+ 0x02A6: "Hcircumflex",
+ 0x02A9: "Iabovedot",
+ 0x02AB: "Gbreve",
+ 0x02AC: "Jcircumflex",
+ 0x02B1: "hstroke",
+ 0x02B6: "hcircumflex",
+ 0x02B9: "idotless",
+ 0x02BB: "gbreve",
+ 0x02BC: "jcircumflex",
+ 0x02C5: "Cabovedot",
+ 0x02C6: "Ccircumflex",
+ 0x02D5: "Gabovedot",
+ 0x02D8: "Gcircumflex",
+ 0x02DD: "Ubreve",
+ 0x02DE: "Scircumflex",
+ 0x02E5: "cabovedot",
+ 0x02E6: "ccircumflex",
+ 0x02F5: "gabovedot",
+ 0x02F8: "gcircumflex",
+ 0x02FD: "ubreve",
+ 0x02FE: "scircumflex",
+ 0x03A2: "kra",
+ 0x03A2: "kappa",
+ 0x03A3: "Rcedilla",
+ 0x03A5: "Itilde",
+ 0x03A6: "Lcedilla",
+ 0x03AA: "Emacron",
+ 0x03AB: "Gcedilla",
+ 0x03AC: "Tslash",
+ 0x03B3: "rcedilla",
+ 0x03B5: "itilde",
+ 0x03B6: "lcedilla",
+ 0x03BA: "emacron",
+ 0x03BB: "gcedilla",
+ 0x03BC: "tslash",
+ 0x03BD: "ENG",
+ 0x03BF: "eng",
+ 0x03C0: "Amacron",
+ 0x03C7: "Iogonek",
+ 0x03CC: "Eabovedot",
+ 0x03CF: "Imacron",
+ 0x03D1: "Ncedilla",
+ 0x03D2: "Omacron",
+ 0x03D3: "Kcedilla",
+ 0x03D9: "Uogonek",
+ 0x03DD: "Utilde",
+ 0x03DE: "Umacron",
+ 0x03E0: "amacron",
+ 0x03E7: "iogonek",
+ 0x03EC: "eabovedot",
+ 0x03EF: "imacron",
+ 0x03F1: "ncedilla",
+ 0x03F2: "omacron",
+ 0x03F3: "kcedilla",
+ 0x03F9: "uogonek",
+ 0x03FD: "utilde",
+ 0x03FE: "umacron",
+ 0x1000174: "Wcircumflex",
+ 0x1000175: "wcircumflex",
+ 0x1000176: "Ycircumflex",
+ 0x1000177: "ycircumflex",
+ 0x1001E02: "Babovedot",
+ 0x1001E03: "babovedot",
+ 0x1001E0A: "Dabovedot",
+ 0x1001E0B: "dabovedot",
+ 0x1001E1E: "Fabovedot",
+ 0x1001E1F: "fabovedot",
+ 0x1001E40: "Mabovedot",
+ 0x1001E41: "mabovedot",
+ 0x1001E56: "Pabovedot",
+ 0x1001E57: "pabovedot",
+ 0x1001E60: "Sabovedot",
+ 0x1001E61: "sabovedot",
+ 0x1001E6A: "Tabovedot",
+ 0x1001E6B: "tabovedot",
+ 0x1001E80: "Wgrave",
+ 0x1001E81: "wgrave",
+ 0x1001E82: "Wacute",
+ 0x1001E83: "wacute",
+ 0x1001E84: "Wdiaeresis",
+ 0x1001E85: "wdiaeresis",
+ 0x1001EF2: "Ygrave",
+ 0x1001EF3: "ygrave",
+ 0x13BC: "OE",
+ 0x13BD: "oe",
+ 0x13BE: "Ydiaeresis",
+ 0x047E: "overline",
+ 0x04A1: "kana_fullstop",
+ 0x04A2: "kana_openingbracket",
+ 0x04A3: "kana_closingbracket",
+ 0x04A4: "kana_comma",
+ 0x04A5: "kana_conjunctive",
+ 0x04A5: "kana_middledot",
+ 0x04A6: "kana_WO",
+ 0x04A7: "kana_a",
+ 0x04A8: "kana_i",
+ 0x04A9: "kana_u",
+ 0x04AA: "kana_e",
+ 0x04AB: "kana_o",
+ 0x04AC: "kana_ya",
+ 0x04AD: "kana_yu",
+ 0x04AE: "kana_yo",
+ 0x04AF: "kana_tsu",
+ 0x04AF: "kana_tu",
+ 0x04B0: "prolongedsound",
+ 0x04B1: "kana_A",
+ 0x04B2: "kana_I",
+ 0x04B3: "kana_U",
+ 0x04B4: "kana_E",
+ 0x04B5: "kana_O",
+ 0x04B6: "kana_KA",
+ 0x04B7: "kana_KI",
+ 0x04B8: "kana_KU",
+ 0x04B9: "kana_KE",
+ 0x04BA: "kana_KO",
+ 0x04BB: "kana_SA",
+ 0x04BC: "kana_SHI",
+ 0x04BD: "kana_SU",
+ 0x04BE: "kana_SE",
+ 0x04BF: "kana_SO",
+ 0x04C0: "kana_TA",
+ 0x04C1: "kana_CHI",
+ 0x04C1: "kana_TI",
+ 0x04C2: "kana_TSU",
+ 0x04C2: "kana_TU",
+ 0x04C3: "kana_TE",
+ 0x04C4: "kana_TO",
+ 0x04C5: "kana_NA",
+ 0x04C6: "kana_NI",
+ 0x04C7: "kana_NU",
+ 0x04C8: "kana_NE",
+ 0x04C9: "kana_NO",
+ 0x04CA: "kana_HA",
+ 0x04CB: "kana_HI",
+ 0x04CC: "kana_FU",
+ 0x04CC: "kana_HU",
+ 0x04CD: "kana_HE",
+ 0x04CE: "kana_HO",
+ 0x04CF: "kana_MA",
+ 0x04D0: "kana_MI",
+ 0x04D1: "kana_MU",
+ 0x04D2: "kana_ME",
+ 0x04D3: "kana_MO",
+ 0x04D4: "kana_YA",
+ 0x04D5: "kana_YU",
+ 0x04D6: "kana_YO",
+ 0x04D7: "kana_RA",
+ 0x04D8: "kana_RI",
+ 0x04D9: "kana_RU",
+ 0x04DA: "kana_RE",
+ 0x04DB: "kana_RO",
+ 0x04DC: "kana_WA",
+ 0x04DD: "kana_N",
+ 0x04DE: "voicedsound",
+ 0x04DF: "semivoicedsound",
+ 0xFF7E: "kana_switch",
+ 0x10006F0: "Farsi_0",
+ 0x10006F1: "Farsi_1",
+ 0x10006F2: "Farsi_2",
+ 0x10006F3: "Farsi_3",
+ 0x10006F4: "Farsi_4",
+ 0x10006F5: "Farsi_5",
+ 0x10006F6: "Farsi_6",
+ 0x10006F7: "Farsi_7",
+ 0x10006F8: "Farsi_8",
+ 0x10006F9: "Farsi_9",
+ 0x100066A: "Arabic_percent",
+ 0x1000670: "Arabic_superscript_alef",
+ 0x1000679: "Arabic_tteh",
+ 0x100067E: "Arabic_peh",
+ 0x1000686: "Arabic_tcheh",
+ 0x1000688: "Arabic_ddal",
+ 0x1000691: "Arabic_rreh",
+ 0x05AC: "Arabic_comma",
+ 0x10006D4: "Arabic_fullstop",
+ 0x1000660: "Arabic_0",
+ 0x1000661: "Arabic_1",
+ 0x1000662: "Arabic_2",
+ 0x1000663: "Arabic_3",
+ 0x1000664: "Arabic_4",
+ 0x1000665: "Arabic_5",
+ 0x1000666: "Arabic_6",
+ 0x1000667: "Arabic_7",
+ 0x1000668: "Arabic_8",
+ 0x1000669: "Arabic_9",
+ 0x05BB: "Arabic_semicolon",
+ 0x05BF: "Arabic_question_mark",
+ 0x05C1: "Arabic_hamza",
+ 0x05C2: "Arabic_maddaonalef",
+ 0x05C3: "Arabic_hamzaonalef",
+ 0x05C4: "Arabic_hamzaonwaw",
+ 0x05C5: "Arabic_hamzaunderalef",
+ 0x05C6: "Arabic_hamzaonyeh",
+ 0x05C7: "Arabic_alef",
+ 0x05C8: "Arabic_beh",
+ 0x05C9: "Arabic_tehmarbuta",
+ 0x05CA: "Arabic_teh",
+ 0x05CB: "Arabic_theh",
+ 0x05CC: "Arabic_jeem",
+ 0x05CD: "Arabic_hah",
+ 0x05CE: "Arabic_khah",
+ 0x05CF: "Arabic_dal",
+ 0x05D0: "Arabic_thal",
+ 0x05D1: "Arabic_ra",
+ 0x05D2: "Arabic_zain",
+ 0x05D3: "Arabic_seen",
+ 0x05D4: "Arabic_sheen",
+ 0x05D5: "Arabic_sad",
+ 0x05D6: "Arabic_dad",
+ 0x05D7: "Arabic_tah",
+ 0x05D8: "Arabic_zah",
+ 0x05D9: "Arabic_ain",
+ 0x05DA: "Arabic_ghain",
+ 0x05E0: "Arabic_tatweel",
+ 0x05E1: "Arabic_feh",
+ 0x05E2: "Arabic_qaf",
+ 0x05E3: "Arabic_kaf",
+ 0x05E4: "Arabic_lam",
+ 0x05E5: "Arabic_meem",
+ 0x05E6: "Arabic_noon",
+ 0x05E7: "Arabic_ha",
+ 0x05E7: "Arabic_heh",
+ 0x05E8: "Arabic_waw",
+ 0x05E9: "Arabic_alefmaksura",
+ 0x05EA: "Arabic_yeh",
+ 0x05EB: "Arabic_fathatan",
+ 0x05EC: "Arabic_dammatan",
+ 0x05ED: "Arabic_kasratan",
+ 0x05EE: "Arabic_fatha",
+ 0x05EF: "Arabic_damma",
+ 0x05F0: "Arabic_kasra",
+ 0x05F1: "Arabic_shadda",
+ 0x05F2: "Arabic_sukun",
+ 0x1000653: "Arabic_madda_above",
+ 0x1000654: "Arabic_hamza_above",
+ 0x1000655: "Arabic_hamza_below",
+ 0x1000698: "Arabic_jeh",
+ 0x10006A4: "Arabic_veh",
+ 0x10006A9: "Arabic_keheh",
+ 0x10006AF: "Arabic_gaf",
+ 0x10006BA: "Arabic_noon_ghunna",
+ 0x10006BE: "Arabic_heh_doachashmee",
+ 0x10006CC: "Farsi_yeh",
+ 0x10006CC: "Arabic_farsi_yeh",
+ 0x10006D2: "Arabic_yeh_baree",
+ 0x10006C1: "Arabic_heh_goal",
+ 0xFF7E: "Arabic_switch",
+ 0x1000492: "Cyrillic_GHE_bar",
+ 0x1000493: "Cyrillic_ghe_bar",
+ 0x1000496: "Cyrillic_ZHE_descender",
+ 0x1000497: "Cyrillic_zhe_descender",
+ 0x100049A: "Cyrillic_KA_descender",
+ 0x100049B: "Cyrillic_ka_descender",
+ 0x100049C: "Cyrillic_KA_vertstroke",
+ 0x100049D: "Cyrillic_ka_vertstroke",
+ 0x10004A2: "Cyrillic_EN_descender",
+ 0x10004A3: "Cyrillic_en_descender",
+ 0x10004AE: "Cyrillic_U_straight",
+ 0x10004AF: "Cyrillic_u_straight",
+ 0x10004B0: "Cyrillic_U_straight_bar",
+ 0x10004B1: "Cyrillic_u_straight_bar",
+ 0x10004B2: "Cyrillic_HA_descender",
+ 0x10004B3: "Cyrillic_ha_descender",
+ 0x10004B6: "Cyrillic_CHE_descender",
+ 0x10004B7: "Cyrillic_che_descender",
+ 0x10004B8: "Cyrillic_CHE_vertstroke",
+ 0x10004B9: "Cyrillic_che_vertstroke",
+ 0x10004BA: "Cyrillic_SHHA",
+ 0x10004BB: "Cyrillic_shha",
+ 0x10004D8: "Cyrillic_SCHWA",
+ 0x10004D9: "Cyrillic_schwa",
+ 0x10004E2: "Cyrillic_I_macron",
+ 0x10004E3: "Cyrillic_i_macron",
+ 0x10004E8: "Cyrillic_O_bar",
+ 0x10004E9: "Cyrillic_o_bar",
+ 0x10004EE: "Cyrillic_U_macron",
+ 0x10004EF: "Cyrillic_u_macron",
+ 0x06A1: "Serbian_dje",
+ 0x06A2: "Macedonia_gje",
+ 0x06A3: "Cyrillic_io",
+ 0x06A4: "Ukrainian_ie",
+ 0x06A4: "Ukranian_je",
+ 0x06A5: "Macedonia_dse",
+ 0x06A6: "Ukrainian_i",
+ 0x06A6: "Ukranian_i",
+ 0x06A7: "Ukrainian_yi",
+ 0x06A7: "Ukranian_yi",
+ 0x06A8: "Cyrillic_je",
+ 0x06A8: "Serbian_je",
+ 0x06A9: "Cyrillic_lje",
+ 0x06A9: "Serbian_lje",
+ 0x06AA: "Cyrillic_nje",
+ 0x06AA: "Serbian_nje",
+ 0x06AB: "Serbian_tshe",
+ 0x06AC: "Macedonia_kje",
+ 0x06AD: "Ukrainian_ghe_with_upturn",
+ 0x06AE: "Byelorussian_shortu",
+ 0x06AF: "Cyrillic_dzhe",
+ 0x06AF: "Serbian_dze",
+ 0x06B0: "numerosign",
+ 0x06B1: "Serbian_DJE",
+ 0x06B2: "Macedonia_GJE",
+ 0x06B3: "Cyrillic_IO",
+ 0x06B4: "Ukrainian_IE",
+ 0x06B4: "Ukranian_JE",
+ 0x06B5: "Macedonia_DSE",
+ 0x06B6: "Ukrainian_I",
+ 0x06B6: "Ukranian_I",
+ 0x06B7: "Ukrainian_YI",
+ 0x06B7: "Ukranian_YI",
+ 0x06B8: "Cyrillic_JE",
+ 0x06B8: "Serbian_JE",
+ 0x06B9: "Cyrillic_LJE",
+ 0x06B9: "Serbian_LJE",
+ 0x06BA: "Cyrillic_NJE",
+ 0x06BA: "Serbian_NJE",
+ 0x06BB: "Serbian_TSHE",
+ 0x06BC: "Macedonia_KJE",
+ 0x06BD: "Ukrainian_GHE_WITH_UPTURN",
+ 0x06BE: "Byelorussian_SHORTU",
+ 0x06BF: "Cyrillic_DZHE",
+ 0x06BF: "Serbian_DZE",
+ 0x06C0: "Cyrillic_yu",
+ 0x06C1: "Cyrillic_a",
+ 0x06C2: "Cyrillic_be",
+ 0x06C3: "Cyrillic_tse",
+ 0x06C4: "Cyrillic_de",
+ 0x06C5: "Cyrillic_ie",
+ 0x06C6: "Cyrillic_ef",
+ 0x06C7: "Cyrillic_ghe",
+ 0x06C8: "Cyrillic_ha",
+ 0x06C9: "Cyrillic_i",
+ 0x06CA: "Cyrillic_shorti",
+ 0x06CB: "Cyrillic_ka",
+ 0x06CC: "Cyrillic_el",
+ 0x06CD: "Cyrillic_em",
+ 0x06CE: "Cyrillic_en",
+ 0x06CF: "Cyrillic_o",
+ 0x06D0: "Cyrillic_pe",
+ 0x06D1: "Cyrillic_ya",
+ 0x06D2: "Cyrillic_er",
+ 0x06D3: "Cyrillic_es",
+ 0x06D4: "Cyrillic_te",
+ 0x06D5: "Cyrillic_u",
+ 0x06D6: "Cyrillic_zhe",
+ 0x06D7: "Cyrillic_ve",
+ 0x06D8: "Cyrillic_softsign",
+ 0x06D9: "Cyrillic_yeru",
+ 0x06DA: "Cyrillic_ze",
+ 0x06DB: "Cyrillic_sha",
+ 0x06DC: "Cyrillic_e",
+ 0x06DD: "Cyrillic_shcha",
+ 0x06DE: "Cyrillic_che",
+ 0x06DF: "Cyrillic_hardsign",
+ 0x06E0: "Cyrillic_YU",
+ 0x06E1: "Cyrillic_A",
+ 0x06E2: "Cyrillic_BE",
+ 0x06E3: "Cyrillic_TSE",
+ 0x06E4: "Cyrillic_DE",
+ 0x06E5: "Cyrillic_IE",
+ 0x06E6: "Cyrillic_EF",
+ 0x06E7: "Cyrillic_GHE",
+ 0x06E8: "Cyrillic_HA",
+ 0x06E9: "Cyrillic_I",
+ 0x06EA: "Cyrillic_SHORTI",
+ 0x06EB: "Cyrillic_KA",
+ 0x06EC: "Cyrillic_EL",
+ 0x06ED: "Cyrillic_EM",
+ 0x06EE: "Cyrillic_EN",
+ 0x06EF: "Cyrillic_O",
+ 0x06F0: "Cyrillic_PE",
+ 0x06F1: "Cyrillic_YA",
+ 0x06F2: "Cyrillic_ER",
+ 0x06F3: "Cyrillic_ES",
+ 0x06F4: "Cyrillic_TE",
+ 0x06F5: "Cyrillic_U",
+ 0x06F6: "Cyrillic_ZHE",
+ 0x06F7: "Cyrillic_VE",
+ 0x06F8: "Cyrillic_SOFTSIGN",
+ 0x06F9: "Cyrillic_YERU",
+ 0x06FA: "Cyrillic_ZE",
+ 0x06FB: "Cyrillic_SHA",
+ 0x06FC: "Cyrillic_E",
+ 0x06FD: "Cyrillic_SHCHA",
+ 0x06FE: "Cyrillic_CHE",
+ 0x06FF: "Cyrillic_HARDSIGN",
+ 0x07A1: "Greek_ALPHAaccent",
+ 0x07A2: "Greek_EPSILONaccent",
+ 0x07A3: "Greek_ETAaccent",
+ 0x07A4: "Greek_IOTAaccent",
+ 0x07A5: "Greek_IOTAdieresis",
+ 0x07A5: "Greek_IOTAdiaeresis",
+ 0x07A7: "Greek_OMICRONaccent",
+ 0x07A8: "Greek_UPSILONaccent",
+ 0x07A9: "Greek_UPSILONdieresis",
+ 0x07AB: "Greek_OMEGAaccent",
+ 0x07AE: "Greek_accentdieresis",
+ 0x07AF: "Greek_horizbar",
+ 0x07B1: "Greek_alphaaccent",
+ 0x07B2: "Greek_epsilonaccent",
+ 0x07B3: "Greek_etaaccent",
+ 0x07B4: "Greek_iotaaccent",
+ 0x07B5: "Greek_iotadieresis",
+ 0x07B6: "Greek_iotaaccentdieresis",
+ 0x07B7: "Greek_omicronaccent",
+ 0x07B8: "Greek_upsilonaccent",
+ 0x07B9: "Greek_upsilondieresis",
+ 0x07BA: "Greek_upsilonaccentdieresis",
+ 0x07BB: "Greek_omegaaccent",
+ 0x07C1: "Greek_ALPHA",
+ 0x07C2: "Greek_BETA",
+ 0x07C3: "Greek_GAMMA",
+ 0x07C4: "Greek_DELTA",
+ 0x07C5: "Greek_EPSILON",
+ 0x07C6: "Greek_ZETA",
+ 0x07C7: "Greek_ETA",
+ 0x07C8: "Greek_THETA",
+ 0x07C9: "Greek_IOTA",
+ 0x07CA: "Greek_KAPPA",
+ 0x07CB: "Greek_LAMDA",
+ 0x07CB: "Greek_LAMBDA",
+ 0x07CC: "Greek_MU",
+ 0x07CD: "Greek_NU",
+ 0x07CE: "Greek_XI",
+ 0x07CF: "Greek_OMICRON",
+ 0x07D0: "Greek_PI",
+ 0x07D1: "Greek_RHO",
+ 0x07D2: "Greek_SIGMA",
+ 0x07D4: "Greek_TAU",
+ 0x07D5: "Greek_UPSILON",
+ 0x07D6: "Greek_PHI",
+ 0x07D7: "Greek_CHI",
+ 0x07D8: "Greek_PSI",
+ 0x07D9: "Greek_OMEGA",
+ 0x07E1: "Greek_alpha",
+ 0x07E2: "Greek_beta",
+ 0x07E3: "Greek_gamma",
+ 0x07E4: "Greek_delta",
+ 0x07E5: "Greek_epsilon",
+ 0x07E6: "Greek_zeta",
+ 0x07E7: "Greek_eta",
+ 0x07E8: "Greek_theta",
+ 0x07E9: "Greek_iota",
+ 0x07EA: "Greek_kappa",
+ 0x07EB: "Greek_lamda",
+ 0x07EB: "Greek_lambda",
+ 0x07EC: "Greek_mu",
+ 0x07ED: "Greek_nu",
+ 0x07EE: "Greek_xi",
+ 0x07EF: "Greek_omicron",
+ 0x07F0: "Greek_pi",
+ 0x07F1: "Greek_rho",
+ 0x07F2: "Greek_sigma",
+ 0x07F3: "Greek_finalsmallsigma",
+ 0x07F4: "Greek_tau",
+ 0x07F5: "Greek_upsilon",
+ 0x07F6: "Greek_phi",
+ 0x07F7: "Greek_chi",
+ 0x07F8: "Greek_psi",
+ 0x07F9: "Greek_omega",
+ 0xFF7E: "Greek_switch",
+ 0x08A1: "leftradical",
+ 0x08A2: "topleftradical",
+ 0x08A3: "horizconnector",
+ 0x08A4: "topintegral",
+ 0x08A5: "botintegral",
+ 0x08A6: "vertconnector",
+ 0x08A7: "topleftsqbracket",
+ 0x08A8: "botleftsqbracket",
+ 0x08A9: "toprightsqbracket",
+ 0x08AA: "botrightsqbracket",
+ 0x08AB: "topleftparens",
+ 0x08AC: "botleftparens",
+ 0x08AD: "toprightparens",
+ 0x08AE: "botrightparens",
+ 0x08AF: "leftmiddlecurlybrace",
+ 0x08B0: "rightmiddlecurlybrace",
+ 0x08B1: "topleftsummation",
+ 0x08B2: "botleftsummation",
+ 0x08B3: "topvertsummationconnector",
+ 0x08B4: "botvertsummationconnector",
+ 0x08B5: "toprightsummation",
+ 0x08B6: "botrightsummation",
+ 0x08B7: "rightmiddlesummation",
+ 0x08BC: "lessthanequal",
+ 0x08BD: "notequal",
+ 0x08BE: "greaterthanequal",
+ 0x08BF: "integral",
+ 0x08C0: "therefore",
+ 0x08C1: "variation",
+ 0x08C2: "infinity",
+ 0x08C5: "nabla",
+ 0x08C8: "approximate",
+ 0x08C9: "similarequal",
+ 0x08CD: "ifonlyif",
+ 0x08CE: "implies",
+ 0x08CF: "identical",
+ 0x08D6: "radical",
+ 0x08DA: "includedin",
+ 0x08DB: "includes",
+ 0x08DC: "intersection",
+ 0x08DD: "union",
+ 0x08DE: "logicaland",
+ 0x08DF: "logicalor",
+ 0x08EF: "partialderivative",
+ 0x08F6: "function",
+ 0x08FB: "leftarrow",
+ 0x08FC: "uparrow",
+ 0x08FD: "rightarrow",
+ 0x08FE: "downarrow",
+ 0x09DF: "blank",
+ 0x09E0: "soliddiamond",
+ 0x09E1: "checkerboard",
+ 0x09E2: "ht",
+ 0x09E3: "ff",
+ 0x09E4: "cr",
+ 0x09E5: "lf",
+ 0x09E8: "nl",
+ 0x09E9: "vt",
+ 0x09EA: "lowrightcorner",
+ 0x09EB: "uprightcorner",
+ 0x09EC: "upleftcorner",
+ 0x09ED: "lowleftcorner",
+ 0x09EE: "crossinglines",
+ 0x09EF: "horizlinescan1",
+ 0x09F0: "horizlinescan3",
+ 0x09F1: "horizlinescan5",
+ 0x09F2: "horizlinescan7",
+ 0x09F3: "horizlinescan9",
+ 0x09F4: "leftt",
+ 0x09F5: "rightt",
+ 0x09F6: "bott",
+ 0x09F7: "topt",
+ 0x09F8: "vertbar",
+ 0x0AA1: "emspace",
+ 0x0AA2: "enspace",
+ 0x0AA3: "em3space",
+ 0x0AA4: "em4space",
+ 0x0AA5: "digitspace",
+ 0x0AA6: "punctspace",
+ 0x0AA7: "thinspace",
+ 0x0AA8: "hairspace",
+ 0x0AA9: "emdash",
+ 0x0AAA: "endash",
+ 0x0AAC: "signifblank",
+ 0x0AAE: "ellipsis",
+ 0x0AAF: "doubbaselinedot",
+ 0x0AB0: "onethird",
+ 0x0AB1: "twothirds",
+ 0x0AB2: "onefifth",
+ 0x0AB3: "twofifths",
+ 0x0AB4: "threefifths",
+ 0x0AB5: "fourfifths",
+ 0x0AB6: "onesixth",
+ 0x0AB7: "fivesixths",
+ 0x0AB8: "careof",
+ 0x0ABB: "figdash",
+ 0x0ABC: "leftanglebracket",
+ 0x0ABD: "decimalpoint",
+ 0x0ABE: "rightanglebracket",
+ 0x0ABF: "marker",
+ 0x0AC3: "oneeighth",
+ 0x0AC4: "threeeighths",
+ 0x0AC5: "fiveeighths",
+ 0x0AC6: "seveneighths",
+ 0x0AC9: "trademark",
+ 0x0ACA: "signaturemark",
+ 0x0ACB: "trademarkincircle",
+ 0x0ACC: "leftopentriangle",
+ 0x0ACD: "rightopentriangle",
+ 0x0ACE: "emopencircle",
+ 0x0ACF: "emopenrectangle",
+ 0x0AD0: "leftsinglequotemark",
+ 0x0AD1: "rightsinglequotemark",
+ 0x0AD2: "leftdoublequotemark",
+ 0x0AD3: "rightdoublequotemark",
+ 0x0AD4: "prescription",
+ 0x0AD5: "permille",
+ 0x0AD6: "minutes",
+ 0x0AD7: "seconds",
+ 0x0AD9: "latincross",
+ 0x0ADA: "hexagram",
+ 0x0ADB: "filledrectbullet",
+ 0x0ADC: "filledlefttribullet",
+ 0x0ADD: "filledrighttribullet",
+ 0x0ADE: "emfilledcircle",
+ 0x0ADF: "emfilledrect",
+ 0x0AE0: "enopencircbullet",
+ 0x0AE1: "enopensquarebullet",
+ 0x0AE2: "openrectbullet",
+ 0x0AE3: "opentribulletup",
+ 0x0AE4: "opentribulletdown",
+ 0x0AE5: "openstar",
+ 0x0AE6: "enfilledcircbullet",
+ 0x0AE7: "enfilledsqbullet",
+ 0x0AE8: "filledtribulletup",
+ 0x0AE9: "filledtribulletdown",
+ 0x0AEA: "leftpointer",
+ 0x0AEB: "rightpointer",
+ 0x0AEC: "club",
+ 0x0AED: "diamond",
+ 0x0AEE: "heart",
+ 0x0AF0: "maltesecross",
+ 0x0AF1: "dagger",
+ 0x0AF2: "doubledagger",
+ 0x0AF3: "checkmark",
+ 0x0AF4: "ballotcross",
+ 0x0AF5: "musicalsharp",
+ 0x0AF6: "musicalflat",
+ 0x0AF7: "malesymbol",
+ 0x0AF8: "femalesymbol",
+ 0x0AF9: "telephone",
+ 0x0AFA: "telephonerecorder",
+ 0x0AFB: "phonographcopyright",
+ 0x0AFC: "caret",
+ 0x0AFD: "singlelowquotemark",
+ 0x0AFE: "doublelowquotemark",
+ 0x0AFF: "cursor",
+ 0x0BA3: "leftcaret",
+ 0x0BA6: "rightcaret",
+ 0x0BA8: "downcaret",
+ 0x0BA9: "upcaret",
+ 0x0BC0: "overbar",
+ 0x0BC2: "downtack",
+ 0x0BC3: "upshoe",
+ 0x0BC4: "downstile",
+ 0x0BC6: "underbar",
+ 0x0BCA: "jot",
+ 0x0BCC: "quad",
+ 0x0BCE: "uptack",
+ 0x0BCF: "circle",
+ 0x0BD3: "upstile",
+ 0x0BD6: "downshoe",
+ 0x0BD8: "rightshoe",
+ 0x0BDA: "leftshoe",
+ 0x0BDC: "lefttack",
+ 0x0BFC: "righttack",
+ 0x0CDF: "hebrew_doublelowline",
+ 0x0CE0: "hebrew_aleph",
+ 0x0CE1: "hebrew_bet",
+ 0x0CE1: "hebrew_beth",
+ 0x0CE2: "hebrew_gimel",
+ 0x0CE2: "hebrew_gimmel",
+ 0x0CE3: "hebrew_dalet",
+ 0x0CE3: "hebrew_daleth",
+ 0x0CE4: "hebrew_he",
+ 0x0CE5: "hebrew_waw",
+ 0x0CE6: "hebrew_zain",
+ 0x0CE6: "hebrew_zayin",
+ 0x0CE7: "hebrew_chet",
+ 0x0CE7: "hebrew_het",
+ 0x0CE8: "hebrew_tet",
+ 0x0CE8: "hebrew_teth",
+ 0x0CE9: "hebrew_yod",
+ 0x0CEA: "hebrew_finalkaph",
+ 0x0CEB: "hebrew_kaph",
+ 0x0CEC: "hebrew_lamed",
+ 0x0CED: "hebrew_finalmem",
+ 0x0CEE: "hebrew_mem",
+ 0x0CEF: "hebrew_finalnun",
+ 0x0CF0: "hebrew_nun",
+ 0x0CF1: "hebrew_samech",
+ 0x0CF1: "hebrew_samekh",
+ 0x0CF2: "hebrew_ayin",
+ 0x0CF3: "hebrew_finalpe",
+ 0x0CF4: "hebrew_pe",
+ 0x0CF5: "hebrew_finalzade",
+ 0x0CF5: "hebrew_finalzadi",
+ 0x0CF6: "hebrew_zade",
+ 0x0CF6: "hebrew_zadi",
+ 0x0CF7: "hebrew_qoph",
+ 0x0CF7: "hebrew_kuf",
+ 0x0CF8: "hebrew_resh",
+ 0x0CF9: "hebrew_shin",
+ 0x0CFA: "hebrew_taw",
+ 0x0CFA: "hebrew_taf",
+ 0xFF7E: "Hebrew_switch",
+ 0x0DA1: "Thai_kokai",
+ 0x0DA2: "Thai_khokhai",
+ 0x0DA3: "Thai_khokhuat",
+ 0x0DA4: "Thai_khokhwai",
+ 0x0DA5: "Thai_khokhon",
+ 0x0DA6: "Thai_khorakhang",
+ 0x0DA7: "Thai_ngongu",
+ 0x0DA8: "Thai_chochan",
+ 0x0DA9: "Thai_choching",
+ 0x0DAA: "Thai_chochang",
+ 0x0DAB: "Thai_soso",
+ 0x0DAC: "Thai_chochoe",
+ 0x0DAD: "Thai_yoying",
+ 0x0DAE: "Thai_dochada",
+ 0x0DAF: "Thai_topatak",
+ 0x0DB0: "Thai_thothan",
+ 0x0DB1: "Thai_thonangmontho",
+ 0x0DB2: "Thai_thophuthao",
+ 0x0DB3: "Thai_nonen",
+ 0x0DB4: "Thai_dodek",
+ 0x0DB5: "Thai_totao",
+ 0x0DB6: "Thai_thothung",
+ 0x0DB7: "Thai_thothahan",
+ 0x0DB8: "Thai_thothong",
+ 0x0DB9: "Thai_nonu",
+ 0x0DBA: "Thai_bobaimai",
+ 0x0DBB: "Thai_popla",
+ 0x0DBC: "Thai_phophung",
+ 0x0DBD: "Thai_fofa",
+ 0x0DBE: "Thai_phophan",
+ 0x0DBF: "Thai_fofan",
+ 0x0DC0: "Thai_phosamphao",
+ 0x0DC1: "Thai_moma",
+ 0x0DC2: "Thai_yoyak",
+ 0x0DC3: "Thai_rorua",
+ 0x0DC4: "Thai_ru",
+ 0x0DC5: "Thai_loling",
+ 0x0DC6: "Thai_lu",
+ 0x0DC7: "Thai_wowaen",
+ 0x0DC8: "Thai_sosala",
+ 0x0DC9: "Thai_sorusi",
+ 0x0DCA: "Thai_sosua",
+ 0x0DCB: "Thai_hohip",
+ 0x0DCC: "Thai_lochula",
+ 0x0DCD: "Thai_oang",
+ 0x0DCE: "Thai_honokhuk",
+ 0x0DCF: "Thai_paiyannoi",
+ 0x0DD0: "Thai_saraa",
+ 0x0DD1: "Thai_maihanakat",
+ 0x0DD2: "Thai_saraaa",
+ 0x0DD3: "Thai_saraam",
+ 0x0DD4: "Thai_sarai",
+ 0x0DD5: "Thai_saraii",
+ 0x0DD6: "Thai_saraue",
+ 0x0DD7: "Thai_sarauee",
+ 0x0DD8: "Thai_sarau",
+ 0x0DD9: "Thai_sarauu",
+ 0x0DDA: "Thai_phinthu",
+ 0x0DDE: "Thai_maihanakat_maitho",
+ 0x0DDF: "Thai_baht",
+ 0x0DE0: "Thai_sarae",
+ 0x0DE1: "Thai_saraae",
+ 0x0DE2: "Thai_sarao",
+ 0x0DE3: "Thai_saraaimaimuan",
+ 0x0DE4: "Thai_saraaimaimalai",
+ 0x0DE5: "Thai_lakkhangyao",
+ 0x0DE6: "Thai_maiyamok",
+ 0x0DE7: "Thai_maitaikhu",
+ 0x0DE8: "Thai_maiek",
+ 0x0DE9: "Thai_maitho",
+ 0x0DEA: "Thai_maitri",
+ 0x0DEB: "Thai_maichattawa",
+ 0x0DEC: "Thai_thanthakhat",
+ 0x0DED: "Thai_nikhahit",
+ 0x0DF0: "Thai_leksun",
+ 0x0DF1: "Thai_leknung",
+ 0x0DF2: "Thai_leksong",
+ 0x0DF3: "Thai_leksam",
+ 0x0DF4: "Thai_leksi",
+ 0x0DF5: "Thai_lekha",
+ 0x0DF6: "Thai_lekhok",
+ 0x0DF7: "Thai_lekchet",
+ 0x0DF8: "Thai_lekpaet",
+ 0x0DF9: "Thai_lekkao",
+ 0xFF31: "Hangul",
+ 0xFF32: "Hangul_Start",
+ 0xFF33: "Hangul_End",
+ 0xFF34: "Hangul_Hanja",
+ 0xFF35: "Hangul_Jamo",
+ 0xFF36: "Hangul_Romaja",
+ 0xFF37: "Hangul_Codeinput",
+ 0xFF38: "Hangul_Jeonja",
+ 0xFF39: "Hangul_Banja",
+ 0xFF3A: "Hangul_PreHanja",
+ 0xFF3B: "Hangul_PostHanja",
+ 0xFF3C: "Hangul_SingleCandidate",
+ 0xFF3D: "Hangul_MultipleCandidate",
+ 0xFF3E: "Hangul_PreviousCandidate",
+ 0xFF3F: "Hangul_Special",
+ 0xFF7E: "Hangul_switch",
+ 0x0EA1: "Hangul_Kiyeog",
+ 0x0EA2: "Hangul_SsangKiyeog",
+ 0x0EA3: "Hangul_KiyeogSios",
+ 0x0EA4: "Hangul_Nieun",
+ 0x0EA5: "Hangul_NieunJieuj",
+ 0x0EA6: "Hangul_NieunHieuh",
+ 0x0EA7: "Hangul_Dikeud",
+ 0x0EA8: "Hangul_SsangDikeud",
+ 0x0EA9: "Hangul_Rieul",
+ 0x0EAA: "Hangul_RieulKiyeog",
+ 0x0EAB: "Hangul_RieulMieum",
+ 0x0EAC: "Hangul_RieulPieub",
+ 0x0EAD: "Hangul_RieulSios",
+ 0x0EAE: "Hangul_RieulTieut",
+ 0x0EAF: "Hangul_RieulPhieuf",
+ 0x0EB0: "Hangul_RieulHieuh",
+ 0x0EB1: "Hangul_Mieum",
+ 0x0EB2: "Hangul_Pieub",
+ 0x0EB3: "Hangul_SsangPieub",
+ 0x0EB4: "Hangul_PieubSios",
+ 0x0EB5: "Hangul_Sios",
+ 0x0EB6: "Hangul_SsangSios",
+ 0x0EB7: "Hangul_Ieung",
+ 0x0EB8: "Hangul_Jieuj",
+ 0x0EB9: "Hangul_SsangJieuj",
+ 0x0EBA: "Hangul_Cieuc",
+ 0x0EBB: "Hangul_Khieuq",
+ 0x0EBC: "Hangul_Tieut",
+ 0x0EBD: "Hangul_Phieuf",
+ 0x0EBE: "Hangul_Hieuh",
+ 0x0EBF: "Hangul_A",
+ 0x0EC0: "Hangul_AE",
+ 0x0EC1: "Hangul_YA",
+ 0x0EC2: "Hangul_YAE",
+ 0x0EC3: "Hangul_EO",
+ 0x0EC4: "Hangul_E",
+ 0x0EC5: "Hangul_YEO",
+ 0x0EC6: "Hangul_YE",
+ 0x0EC7: "Hangul_O",
+ 0x0EC8: "Hangul_WA",
+ 0x0EC9: "Hangul_WAE",
+ 0x0ECA: "Hangul_OE",
+ 0x0ECB: "Hangul_YO",
+ 0x0ECC: "Hangul_U",
+ 0x0ECD: "Hangul_WEO",
+ 0x0ECE: "Hangul_WE",
+ 0x0ECF: "Hangul_WI",
+ 0x0ED0: "Hangul_YU",
+ 0x0ED1: "Hangul_EU",
+ 0x0ED2: "Hangul_YI",
+ 0x0ED3: "Hangul_I",
+ 0x0ED4: "Hangul_J_Kiyeog",
+ 0x0ED5: "Hangul_J_SsangKiyeog",
+ 0x0ED6: "Hangul_J_KiyeogSios",
+ 0x0ED7: "Hangul_J_Nieun",
+ 0x0ED8: "Hangul_J_NieunJieuj",
+ 0x0ED9: "Hangul_J_NieunHieuh",
+ 0x0EDA: "Hangul_J_Dikeud",
+ 0x0EDB: "Hangul_J_Rieul",
+ 0x0EDC: "Hangul_J_RieulKiyeog",
+ 0x0EDD: "Hangul_J_RieulMieum",
+ 0x0EDE: "Hangul_J_RieulPieub",
+ 0x0EDF: "Hangul_J_RieulSios",
+ 0x0EE0: "Hangul_J_RieulTieut",
+ 0x0EE1: "Hangul_J_RieulPhieuf",
+ 0x0EE2: "Hangul_J_RieulHieuh",
+ 0x0EE3: "Hangul_J_Mieum",
+ 0x0EE4: "Hangul_J_Pieub",
+ 0x0EE5: "Hangul_J_PieubSios",
+ 0x0EE6: "Hangul_J_Sios",
+ 0x0EE7: "Hangul_J_SsangSios",
+ 0x0EE8: "Hangul_J_Ieung",
+ 0x0EE9: "Hangul_J_Jieuj",
+ 0x0EEA: "Hangul_J_Cieuc",
+ 0x0EEB: "Hangul_J_Khieuq",
+ 0x0EEC: "Hangul_J_Tieut",
+ 0x0EED: "Hangul_J_Phieuf",
+ 0x0EEE: "Hangul_J_Hieuh",
+ 0x0EEF: "Hangul_RieulYeorinHieuh",
+ 0x0EF0: "Hangul_SunkyeongeumMieum",
+ 0x0EF1: "Hangul_SunkyeongeumPieub",
+ 0x0EF2: "Hangul_PanSios",
+ 0x0EF3: "Hangul_KkogjiDalrinIeung",
+ 0x0EF4: "Hangul_SunkyeongeumPhieuf",
+ 0x0EF5: "Hangul_YeorinHieuh",
+ 0x0EF6: "Hangul_AraeA",
+ 0x0EF7: "Hangul_AraeAE",
+ 0x0EF8: "Hangul_J_PanSios",
+ 0x0EF9: "Hangul_J_KkogjiDalrinIeung",
+ 0x0EFA: "Hangul_J_YeorinHieuh",
+ 0x0EFF: "Korean_Won",
+ 0x1000587: "Armenian_ligature_ew",
+ 0x1000589: "Armenian_full_stop",
+ 0x1000589: "Armenian_verjaket",
+ 0x100055D: "Armenian_separation_mark",
+ 0x100055D: "Armenian_but",
+ 0x100058A: "Armenian_hyphen",
+ 0x100058A: "Armenian_yentamna",
+ 0x100055C: "Armenian_exclam",
+ 0x100055C: "Armenian_amanak",
+ 0x100055B: "Armenian_accent",
+ 0x100055B: "Armenian_shesht",
+ 0x100055E: "Armenian_question",
+ 0x100055E: "Armenian_paruyk",
+ 0x1000531: "Armenian_AYB",
+ 0x1000561: "Armenian_ayb",
+ 0x1000532: "Armenian_BEN",
+ 0x1000562: "Armenian_ben",
+ 0x1000533: "Armenian_GIM",
+ 0x1000563: "Armenian_gim",
+ 0x1000534: "Armenian_DA",
+ 0x1000564: "Armenian_da",
+ 0x1000535: "Armenian_YECH",
+ 0x1000565: "Armenian_yech",
+ 0x1000536: "Armenian_ZA",
+ 0x1000566: "Armenian_za",
+ 0x1000537: "Armenian_E",
+ 0x1000567: "Armenian_e",
+ 0x1000538: "Armenian_AT",
+ 0x1000568: "Armenian_at",
+ 0x1000539: "Armenian_TO",
+ 0x1000569: "Armenian_to",
+ 0x100053A: "Armenian_ZHE",
+ 0x100056A: "Armenian_zhe",
+ 0x100053B: "Armenian_INI",
+ 0x100056B: "Armenian_ini",
+ 0x100053C: "Armenian_LYUN",
+ 0x100056C: "Armenian_lyun",
+ 0x100053D: "Armenian_KHE",
+ 0x100056D: "Armenian_khe",
+ 0x100053E: "Armenian_TSA",
+ 0x100056E: "Armenian_tsa",
+ 0x100053F: "Armenian_KEN",
+ 0x100056F: "Armenian_ken",
+ 0x1000540: "Armenian_HO",
+ 0x1000570: "Armenian_ho",
+ 0x1000541: "Armenian_DZA",
+ 0x1000571: "Armenian_dza",
+ 0x1000542: "Armenian_GHAT",
+ 0x1000572: "Armenian_ghat",
+ 0x1000543: "Armenian_TCHE",
+ 0x1000573: "Armenian_tche",
+ 0x1000544: "Armenian_MEN",
+ 0x1000574: "Armenian_men",
+ 0x1000545: "Armenian_HI",
+ 0x1000575: "Armenian_hi",
+ 0x1000546: "Armenian_NU",
+ 0x1000576: "Armenian_nu",
+ 0x1000547: "Armenian_SHA",
+ 0x1000577: "Armenian_sha",
+ 0x1000548: "Armenian_VO",
+ 0x1000578: "Armenian_vo",
+ 0x1000549: "Armenian_CHA",
+ 0x1000579: "Armenian_cha",
+ 0x100054A: "Armenian_PE",
+ 0x100057A: "Armenian_pe",
+ 0x100054B: "Armenian_JE",
+ 0x100057B: "Armenian_je",
+ 0x100054C: "Armenian_RA",
+ 0x100057C: "Armenian_ra",
+ 0x100054D: "Armenian_SE",
+ 0x100057D: "Armenian_se",
+ 0x100054E: "Armenian_VEV",
+ 0x100057E: "Armenian_vev",
+ 0x100054F: "Armenian_TYUN",
+ 0x100057F: "Armenian_tyun",
+ 0x1000550: "Armenian_RE",
+ 0x1000580: "Armenian_re",
+ 0x1000551: "Armenian_TSO",
+ 0x1000581: "Armenian_tso",
+ 0x1000552: "Armenian_VYUN",
+ 0x1000582: "Armenian_vyun",
+ 0x1000553: "Armenian_PYUR",
+ 0x1000583: "Armenian_pyur",
+ 0x1000554: "Armenian_KE",
+ 0x1000584: "Armenian_ke",
+ 0x1000555: "Armenian_O",
+ 0x1000585: "Armenian_o",
+ 0x1000556: "Armenian_FE",
+ 0x1000586: "Armenian_fe",
+ 0x100055A: "Armenian_apostrophe",
+ 0x10010D0: "Georgian_an",
+ 0x10010D1: "Georgian_ban",
+ 0x10010D2: "Georgian_gan",
+ 0x10010D3: "Georgian_don",
+ 0x10010D4: "Georgian_en",
+ 0x10010D5: "Georgian_vin",
+ 0x10010D6: "Georgian_zen",
+ 0x10010D7: "Georgian_tan",
+ 0x10010D8: "Georgian_in",
+ 0x10010D9: "Georgian_kan",
+ 0x10010DA: "Georgian_las",
+ 0x10010DB: "Georgian_man",
+ 0x10010DC: "Georgian_nar",
+ 0x10010DD: "Georgian_on",
+ 0x10010DE: "Georgian_par",
+ 0x10010DF: "Georgian_zhar",
+ 0x10010E0: "Georgian_rae",
+ 0x10010E1: "Georgian_san",
+ 0x10010E2: "Georgian_tar",
+ 0x10010E3: "Georgian_un",
+ 0x10010E4: "Georgian_phar",
+ 0x10010E5: "Georgian_khar",
+ 0x10010E6: "Georgian_ghan",
+ 0x10010E7: "Georgian_qar",
+ 0x10010E8: "Georgian_shin",
+ 0x10010E9: "Georgian_chin",
+ 0x10010EA: "Georgian_can",
+ 0x10010EB: "Georgian_jil",
+ 0x10010EC: "Georgian_cil",
+ 0x10010ED: "Georgian_char",
+ 0x10010EE: "Georgian_xan",
+ 0x10010EF: "Georgian_jhan",
+ 0x10010F0: "Georgian_hae",
+ 0x10010F1: "Georgian_he",
+ 0x10010F2: "Georgian_hie",
+ 0x10010F3: "Georgian_we",
+ 0x10010F4: "Georgian_har",
+ 0x10010F5: "Georgian_hoe",
+ 0x10010F6: "Georgian_fi",
+ 0x1001E8A: "Xabovedot",
+ 0x100012C: "Ibreve",
+ 0x10001B5: "Zstroke",
+ 0x10001E6: "Gcaron",
+ 0x10001D1: "Ocaron",
+ 0x100019F: "Obarred",
+ 0x1001E8B: "xabovedot",
+ 0x100012D: "ibreve",
+ 0x10001B6: "zstroke",
+ 0x10001E7: "gcaron",
+ 0x10001D2: "ocaron",
+ 0x1000275: "obarred",
+ 0x100018F: "SCHWA",
+ 0x1000259: "schwa",
+ 0x10001B7: "EZH",
+ 0x1000292: "ezh",
+ 0x1001E36: "Lbelowdot",
+ 0x1001E37: "lbelowdot",
+ 0x1001EA0: "Abelowdot",
+ 0x1001EA1: "abelowdot",
+ 0x1001EA2: "Ahook",
+ 0x1001EA3: "ahook",
+ 0x1001EA4: "Acircumflexacute",
+ 0x1001EA5: "acircumflexacute",
+ 0x1001EA6: "Acircumflexgrave",
+ 0x1001EA7: "acircumflexgrave",
+ 0x1001EA8: "Acircumflexhook",
+ 0x1001EA9: "acircumflexhook",
+ 0x1001EAA: "Acircumflextilde",
+ 0x1001EAB: "acircumflextilde",
+ 0x1001EAC: "Acircumflexbelowdot",
+ 0x1001EAD: "acircumflexbelowdot",
+ 0x1001EAE: "Abreveacute",
+ 0x1001EAF: "abreveacute",
+ 0x1001EB0: "Abrevegrave",
+ 0x1001EB1: "abrevegrave",
+ 0x1001EB2: "Abrevehook",
+ 0x1001EB3: "abrevehook",
+ 0x1001EB4: "Abrevetilde",
+ 0x1001EB5: "abrevetilde",
+ 0x1001EB6: "Abrevebelowdot",
+ 0x1001EB7: "abrevebelowdot",
+ 0x1001EB8: "Ebelowdot",
+ 0x1001EB9: "ebelowdot",
+ 0x1001EBA: "Ehook",
+ 0x1001EBB: "ehook",
+ 0x1001EBC: "Etilde",
+ 0x1001EBD: "etilde",
+ 0x1001EBE: "Ecircumflexacute",
+ 0x1001EBF: "ecircumflexacute",
+ 0x1001EC0: "Ecircumflexgrave",
+ 0x1001EC1: "ecircumflexgrave",
+ 0x1001EC2: "Ecircumflexhook",
+ 0x1001EC3: "ecircumflexhook",
+ 0x1001EC4: "Ecircumflextilde",
+ 0x1001EC5: "ecircumflextilde",
+ 0x1001EC6: "Ecircumflexbelowdot",
+ 0x1001EC7: "ecircumflexbelowdot",
+ 0x1001EC8: "Ihook",
+ 0x1001EC9: "ihook",
+ 0x1001ECA: "Ibelowdot",
+ 0x1001ECB: "ibelowdot",
+ 0x1001ECC: "Obelowdot",
+ 0x1001ECD: "obelowdot",
+ 0x1001ECE: "Ohook",
+ 0x1001ECF: "ohook",
+ 0x1001ED0: "Ocircumflexacute",
+ 0x1001ED1: "ocircumflexacute",
+ 0x1001ED2: "Ocircumflexgrave",
+ 0x1001ED3: "ocircumflexgrave",
+ 0x1001ED4: "Ocircumflexhook",
+ 0x1001ED5: "ocircumflexhook",
+ 0x1001ED6: "Ocircumflextilde",
+ 0x1001ED7: "ocircumflextilde",
+ 0x1001ED8: "Ocircumflexbelowdot",
+ 0x1001ED9: "ocircumflexbelowdot",
+ 0x1001EDA: "Ohornacute",
+ 0x1001EDB: "ohornacute",
+ 0x1001EDC: "Ohorngrave",
+ 0x1001EDD: "ohorngrave",
+ 0x1001EDE: "Ohornhook",
+ 0x1001EDF: "ohornhook",
+ 0x1001EE0: "Ohorntilde",
+ 0x1001EE1: "ohorntilde",
+ 0x1001EE2: "Ohornbelowdot",
+ 0x1001EE3: "ohornbelowdot",
+ 0x1001EE4: "Ubelowdot",
+ 0x1001EE5: "ubelowdot",
+ 0x1001EE6: "Uhook",
+ 0x1001EE7: "uhook",
+ 0x1001EE8: "Uhornacute",
+ 0x1001EE9: "uhornacute",
+ 0x1001EEA: "Uhorngrave",
+ 0x1001EEB: "uhorngrave",
+ 0x1001EEC: "Uhornhook",
+ 0x1001EED: "uhornhook",
+ 0x1001EEE: "Uhorntilde",
+ 0x1001EEF: "uhorntilde",
+ 0x1001EF0: "Uhornbelowdot",
+ 0x1001EF1: "uhornbelowdot",
+ 0x1001EF4: "Ybelowdot",
+ 0x1001EF5: "ybelowdot",
+ 0x1001EF6: "Yhook",
+ 0x1001EF7: "yhook",
+ 0x1001EF8: "Ytilde",
+ 0x1001EF9: "ytilde",
+ 0x10001A0: "Ohorn",
+ 0x10001A1: "ohorn",
+ 0x10001AF: "Uhorn",
+ 0x10001B0: "uhorn",
+ 0x10020A0: "EcuSign",
+ 0x10020A1: "ColonSign",
+ 0x10020A2: "CruzeiroSign",
+ 0x10020A3: "FFrancSign",
+ 0x10020A4: "LiraSign",
+ 0x10020A5: "MillSign",
+ 0x10020A6: "NairaSign",
+ 0x10020A7: "PesetaSign",
+ 0x10020A8: "RupeeSign",
+ 0x10020A9: "WonSign",
+ 0x10020AA: "NewSheqelSign",
+ 0x10020AB: "DongSign",
+ 0x20AC: "EuroSign",
+ 0x1002070: "zerosuperior",
+ 0x1002074: "foursuperior",
+ 0x1002075: "fivesuperior",
+ 0x1002076: "sixsuperior",
+ 0x1002077: "sevensuperior",
+ 0x1002078: "eightsuperior",
+ 0x1002079: "ninesuperior",
+ 0x1002080: "zerosubscript",
+ 0x1002081: "onesubscript",
+ 0x1002082: "twosubscript",
+ 0x1002083: "threesubscript",
+ 0x1002084: "foursubscript",
+ 0x1002085: "fivesubscript",
+ 0x1002086: "sixsubscript",
+ 0x1002087: "sevensubscript",
+ 0x1002088: "eightsubscript",
+ 0x1002089: "ninesubscript",
+ 0x1002202: "partdifferential",
+ 0x1002205: "emptyset",
+ 0x1002208: "elementof",
+ 0x1002209: "notelementof",
+ 0x100220B: "containsas",
+ 0x100221A: "squareroot",
+ 0x100221B: "cuberoot",
+ 0x100221C: "fourthroot",
+ 0x100222C: "dintegral",
+ 0x100222D: "tintegral",
+ 0x1002235: "because",
+ 0x1002248: "approxeq",
+ 0x1002247: "notapproxeq",
+ 0x1002262: "notidentical",
+ 0x1002263: "stricteq",
+ 0xFFF1: "braille_dot_1",
+ 0xFFF2: "braille_dot_2",
+ 0xFFF3: "braille_dot_3",
+ 0xFFF4: "braille_dot_4",
+ 0xFFF5: "braille_dot_5",
+ 0xFFF6: "braille_dot_6",
+ 0xFFF7: "braille_dot_7",
+ 0xFFF8: "braille_dot_8",
+ 0xFFF9: "braille_dot_9",
+ 0xFFFA: "braille_dot_10",
+ 0x1002800: "braille_blank",
+ 0x1002801: "braille_dots_1",
+ 0x1002802: "braille_dots_2",
+ 0x1002803: "braille_dots_12",
+ 0x1002804: "braille_dots_3",
+ 0x1002805: "braille_dots_13",
+ 0x1002806: "braille_dots_23",
+ 0x1002807: "braille_dots_123",
+ 0x1002808: "braille_dots_4",
+ 0x1002809: "braille_dots_14",
+ 0x100280A: "braille_dots_24",
+ 0x100280B: "braille_dots_124",
+ 0x100280C: "braille_dots_34",
+ 0x100280D: "braille_dots_134",
+ 0x100280E: "braille_dots_234",
+ 0x100280F: "braille_dots_1234",
+ 0x1002810: "braille_dots_5",
+ 0x1002811: "braille_dots_15",
+ 0x1002812: "braille_dots_25",
+ 0x1002813: "braille_dots_125",
+ 0x1002814: "braille_dots_35",
+ 0x1002815: "braille_dots_135",
+ 0x1002816: "braille_dots_235",
+ 0x1002817: "braille_dots_1235",
+ 0x1002818: "braille_dots_45",
+ 0x1002819: "braille_dots_145",
+ 0x100281A: "braille_dots_245",
+ 0x100281B: "braille_dots_1245",
+ 0x100281C: "braille_dots_345",
+ 0x100281D: "braille_dots_1345",
+ 0x100281E: "braille_dots_2345",
+ 0x100281F: "braille_dots_12345",
+ 0x1002820: "braille_dots_6",
+ 0x1002821: "braille_dots_16",
+ 0x1002822: "braille_dots_26",
+ 0x1002823: "braille_dots_126",
+ 0x1002824: "braille_dots_36",
+ 0x1002825: "braille_dots_136",
+ 0x1002826: "braille_dots_236",
+ 0x1002827: "braille_dots_1236",
+ 0x1002828: "braille_dots_46",
+ 0x1002829: "braille_dots_146",
+ 0x100282A: "braille_dots_246",
+ 0x100282B: "braille_dots_1246",
+ 0x100282C: "braille_dots_346",
+ 0x100282D: "braille_dots_1346",
+ 0x100282E: "braille_dots_2346",
+ 0x100282F: "braille_dots_12346",
+ 0x1002830: "braille_dots_56",
+ 0x1002831: "braille_dots_156",
+ 0x1002832: "braille_dots_256",
+ 0x1002833: "braille_dots_1256",
+ 0x1002834: "braille_dots_356",
+ 0x1002835: "braille_dots_1356",
+ 0x1002836: "braille_dots_2356",
+ 0x1002837: "braille_dots_12356",
+ 0x1002838: "braille_dots_456",
+ 0x1002839: "braille_dots_1456",
+ 0x100283A: "braille_dots_2456",
+ 0x100283B: "braille_dots_12456",
+ 0x100283C: "braille_dots_3456",
+ 0x100283D: "braille_dots_13456",
+ 0x100283E: "braille_dots_23456",
+ 0x100283F: "braille_dots_123456",
+ 0x1002840: "braille_dots_7",
+ 0x1002841: "braille_dots_17",
+ 0x1002842: "braille_dots_27",
+ 0x1002843: "braille_dots_127",
+ 0x1002844: "braille_dots_37",
+ 0x1002845: "braille_dots_137",
+ 0x1002846: "braille_dots_237",
+ 0x1002847: "braille_dots_1237",
+ 0x1002848: "braille_dots_47",
+ 0x1002849: "braille_dots_147",
+ 0x100284A: "braille_dots_247",
+ 0x100284B: "braille_dots_1247",
+ 0x100284C: "braille_dots_347",
+ 0x100284D: "braille_dots_1347",
+ 0x100284E: "braille_dots_2347",
+ 0x100284F: "braille_dots_12347",
+ 0x1002850: "braille_dots_57",
+ 0x1002851: "braille_dots_157",
+ 0x1002852: "braille_dots_257",
+ 0x1002853: "braille_dots_1257",
+ 0x1002854: "braille_dots_357",
+ 0x1002855: "braille_dots_1357",
+ 0x1002856: "braille_dots_2357",
+ 0x1002857: "braille_dots_12357",
+ 0x1002858: "braille_dots_457",
+ 0x1002859: "braille_dots_1457",
+ 0x100285A: "braille_dots_2457",
+ 0x100285B: "braille_dots_12457",
+ 0x100285C: "braille_dots_3457",
+ 0x100285D: "braille_dots_13457",
+ 0x100285E: "braille_dots_23457",
+ 0x100285F: "braille_dots_123457",
+ 0x1002860: "braille_dots_67",
+ 0x1002861: "braille_dots_167",
+ 0x1002862: "braille_dots_267",
+ 0x1002863: "braille_dots_1267",
+ 0x1002864: "braille_dots_367",
+ 0x1002865: "braille_dots_1367",
+ 0x1002866: "braille_dots_2367",
+ 0x1002867: "braille_dots_12367",
+ 0x1002868: "braille_dots_467",
+ 0x1002869: "braille_dots_1467",
+ 0x100286A: "braille_dots_2467",
+ 0x100286B: "braille_dots_12467",
+ 0x100286C: "braille_dots_3467",
+ 0x100286D: "braille_dots_13467",
+ 0x100286E: "braille_dots_23467",
+ 0x100286F: "braille_dots_123467",
+ 0x1002870: "braille_dots_567",
+ 0x1002871: "braille_dots_1567",
+ 0x1002872: "braille_dots_2567",
+ 0x1002873: "braille_dots_12567",
+ 0x1002874: "braille_dots_3567",
+ 0x1002875: "braille_dots_13567",
+ 0x1002876: "braille_dots_23567",
+ 0x1002877: "braille_dots_123567",
+ 0x1002878: "braille_dots_4567",
+ 0x1002879: "braille_dots_14567",
+ 0x100287A: "braille_dots_24567",
+ 0x100287B: "braille_dots_124567",
+ 0x100287C: "braille_dots_34567",
+ 0x100287D: "braille_dots_134567",
+ 0x100287E: "braille_dots_234567",
+ 0x100287F: "braille_dots_1234567",
+ 0x1002880: "braille_dots_8",
+ 0x1002881: "braille_dots_18",
+ 0x1002882: "braille_dots_28",
+ 0x1002883: "braille_dots_128",
+ 0x1002884: "braille_dots_38",
+ 0x1002885: "braille_dots_138",
+ 0x1002886: "braille_dots_238",
+ 0x1002887: "braille_dots_1238",
+ 0x1002888: "braille_dots_48",
+ 0x1002889: "braille_dots_148",
+ 0x100288A: "braille_dots_248",
+ 0x100288B: "braille_dots_1248",
+ 0x100288C: "braille_dots_348",
+ 0x100288D: "braille_dots_1348",
+ 0x100288E: "braille_dots_2348",
+ 0x100288F: "braille_dots_12348",
+ 0x1002890: "braille_dots_58",
+ 0x1002891: "braille_dots_158",
+ 0x1002892: "braille_dots_258",
+ 0x1002893: "braille_dots_1258",
+ 0x1002894: "braille_dots_358",
+ 0x1002895: "braille_dots_1358",
+ 0x1002896: "braille_dots_2358",
+ 0x1002897: "braille_dots_12358",
+ 0x1002898: "braille_dots_458",
+ 0x1002899: "braille_dots_1458",
+ 0x100289A: "braille_dots_2458",
+ 0x100289B: "braille_dots_12458",
+ 0x100289C: "braille_dots_3458",
+ 0x100289D: "braille_dots_13458",
+ 0x100289E: "braille_dots_23458",
+ 0x100289F: "braille_dots_123458",
+ 0x10028A0: "braille_dots_68",
+ 0x10028A1: "braille_dots_168",
+ 0x10028A2: "braille_dots_268",
+ 0x10028A3: "braille_dots_1268",
+ 0x10028A4: "braille_dots_368",
+ 0x10028A5: "braille_dots_1368",
+ 0x10028A6: "braille_dots_2368",
+ 0x10028A7: "braille_dots_12368",
+ 0x10028A8: "braille_dots_468",
+ 0x10028A9: "braille_dots_1468",
+ 0x10028AA: "braille_dots_2468",
+ 0x10028AB: "braille_dots_12468",
+ 0x10028AC: "braille_dots_3468",
+ 0x10028AD: "braille_dots_13468",
+ 0x10028AE: "braille_dots_23468",
+ 0x10028AF: "braille_dots_123468",
+ 0x10028B0: "braille_dots_568",
+ 0x10028B1: "braille_dots_1568",
+ 0x10028B2: "braille_dots_2568",
+ 0x10028B3: "braille_dots_12568",
+ 0x10028B4: "braille_dots_3568",
+ 0x10028B5: "braille_dots_13568",
+ 0x10028B6: "braille_dots_23568",
+ 0x10028B7: "braille_dots_123568",
+ 0x10028B8: "braille_dots_4568",
+ 0x10028B9: "braille_dots_14568",
+ 0x10028BA: "braille_dots_24568",
+ 0x10028BB: "braille_dots_124568",
+ 0x10028BC: "braille_dots_34568",
+ 0x10028BD: "braille_dots_134568",
+ 0x10028BE: "braille_dots_234568",
+ 0x10028BF: "braille_dots_1234568",
+ 0x10028C0: "braille_dots_78",
+ 0x10028C1: "braille_dots_178",
+ 0x10028C2: "braille_dots_278",
+ 0x10028C3: "braille_dots_1278",
+ 0x10028C4: "braille_dots_378",
+ 0x10028C5: "braille_dots_1378",
+ 0x10028C6: "braille_dots_2378",
+ 0x10028C7: "braille_dots_12378",
+ 0x10028C8: "braille_dots_478",
+ 0x10028C9: "braille_dots_1478",
+ 0x10028CA: "braille_dots_2478",
+ 0x10028CB: "braille_dots_12478",
+ 0x10028CC: "braille_dots_3478",
+ 0x10028CD: "braille_dots_13478",
+ 0x10028CE: "braille_dots_23478",
+ 0x10028CF: "braille_dots_123478",
+ 0x10028D0: "braille_dots_578",
+ 0x10028D1: "braille_dots_1578",
+ 0x10028D2: "braille_dots_2578",
+ 0x10028D3: "braille_dots_12578",
+ 0x10028D4: "braille_dots_3578",
+ 0x10028D5: "braille_dots_13578",
+ 0x10028D6: "braille_dots_23578",
+ 0x10028D7: "braille_dots_123578",
+ 0x10028D8: "braille_dots_4578",
+ 0x10028D9: "braille_dots_14578",
+ 0x10028DA: "braille_dots_24578",
+ 0x10028DB: "braille_dots_124578",
+ 0x10028DC: "braille_dots_34578",
+ 0x10028DD: "braille_dots_134578",
+ 0x10028DE: "braille_dots_234578",
+ 0x10028DF: "braille_dots_1234578",
+ 0x10028E0: "braille_dots_678",
+ 0x10028E1: "braille_dots_1678",
+ 0x10028E2: "braille_dots_2678",
+ 0x10028E3: "braille_dots_12678",
+ 0x10028E4: "braille_dots_3678",
+ 0x10028E5: "braille_dots_13678",
+ 0x10028E6: "braille_dots_23678",
+ 0x10028E7: "braille_dots_123678",
+ 0x10028E8: "braille_dots_4678",
+ 0x10028E9: "braille_dots_14678",
+ 0x10028EA: "braille_dots_24678",
+ 0x10028EB: "braille_dots_124678",
+ 0x10028EC: "braille_dots_34678",
+ 0x10028ED: "braille_dots_134678",
+ 0x10028EE: "braille_dots_234678",
+ 0x10028EF: "braille_dots_1234678",
+ 0x10028F0: "braille_dots_5678",
+ 0x10028F1: "braille_dots_15678",
+ 0x10028F2: "braille_dots_25678",
+ 0x10028F3: "braille_dots_125678",
+ 0x10028F4: "braille_dots_35678",
+ 0x10028F5: "braille_dots_135678",
+ 0x10028F6: "braille_dots_235678",
+ 0x10028F7: "braille_dots_1235678",
+ 0x10028F8: "braille_dots_45678",
+ 0x10028F9: "braille_dots_145678",
+ 0x10028FA: "braille_dots_245678",
+ 0x10028FB: "braille_dots_1245678",
+ 0x10028FC: "braille_dots_345678",
+ 0x10028FD: "braille_dots_1345678",
+ 0x10028FE: "braille_dots_2345678",
+ 0x10028FF: "braille_dots_12345678",
+ 0x1000D82: "Sinh_ng",
+ 0x1000D83: "Sinh_h2",
+ 0x1000D85: "Sinh_a",
+ 0x1000D86: "Sinh_aa",
+ 0x1000D87: "Sinh_ae",
+ 0x1000D88: "Sinh_aee",
+ 0x1000D89: "Sinh_i",
+ 0x1000D8A: "Sinh_ii",
+ 0x1000D8B: "Sinh_u",
+ 0x1000D8C: "Sinh_uu",
+ 0x1000D8D: "Sinh_ri",
+ 0x1000D8E: "Sinh_rii",
+ 0x1000D8F: "Sinh_lu",
+ 0x1000D90: "Sinh_luu",
+ 0x1000D91: "Sinh_e",
+ 0x1000D92: "Sinh_ee",
+ 0x1000D93: "Sinh_ai",
+ 0x1000D94: "Sinh_o",
+ 0x1000D95: "Sinh_oo",
+ 0x1000D96: "Sinh_au",
+ 0x1000D9A: "Sinh_ka",
+ 0x1000D9B: "Sinh_kha",
+ 0x1000D9C: "Sinh_ga",
+ 0x1000D9D: "Sinh_gha",
+ 0x1000D9E: "Sinh_ng2",
+ 0x1000D9F: "Sinh_nga",
+ 0x1000DA0: "Sinh_ca",
+ 0x1000DA1: "Sinh_cha",
+ 0x1000DA2: "Sinh_ja",
+ 0x1000DA3: "Sinh_jha",
+ 0x1000DA4: "Sinh_nya",
+ 0x1000DA5: "Sinh_jnya",
+ 0x1000DA6: "Sinh_nja",
+ 0x1000DA7: "Sinh_tta",
+ 0x1000DA8: "Sinh_ttha",
+ 0x1000DA9: "Sinh_dda",
+ 0x1000DAA: "Sinh_ddha",
+ 0x1000DAB: "Sinh_nna",
+ 0x1000DAC: "Sinh_ndda",
+ 0x1000DAD: "Sinh_tha",
+ 0x1000DAE: "Sinh_thha",
+ 0x1000DAF: "Sinh_dha",
+ 0x1000DB0: "Sinh_dhha",
+ 0x1000DB1: "Sinh_na",
+ 0x1000DB3: "Sinh_ndha",
+ 0x1000DB4: "Sinh_pa",
+ 0x1000DB5: "Sinh_pha",
+ 0x1000DB6: "Sinh_ba",
+ 0x1000DB7: "Sinh_bha",
+ 0x1000DB8: "Sinh_ma",
+ 0x1000DB9: "Sinh_mba",
+ 0x1000DBA: "Sinh_ya",
+ 0x1000DBB: "Sinh_ra",
+ 0x1000DBD: "Sinh_la",
+ 0x1000DC0: "Sinh_va",
+ 0x1000DC1: "Sinh_sha",
+ 0x1000DC2: "Sinh_ssha",
+ 0x1000DC3: "Sinh_sa",
+ 0x1000DC4: "Sinh_ha",
+ 0x1000DC5: "Sinh_lla",
+ 0x1000DC6: "Sinh_fa",
+ 0x1000DCA: "Sinh_al",
+ 0x1000DCF: "Sinh_aa2",
+ 0x1000DD0: "Sinh_ae2",
+ 0x1000DD1: "Sinh_aee2",
+ 0x1000DD2: "Sinh_i2",
+ 0x1000DD3: "Sinh_ii2",
+ 0x1000DD4: "Sinh_u2",
+ 0x1000DD6: "Sinh_uu2",
+ 0x1000DD8: "Sinh_ru2",
+ 0x1000DD9: "Sinh_e2",
+ 0x1000DDA: "Sinh_ee2",
+ 0x1000DDB: "Sinh_ai2",
+ 0x1000DDC: "Sinh_o2",
+ 0x1000DDD: "Sinh_oo2",
+ 0x1000DDE: "Sinh_au2",
+ 0x1000DDF: "Sinh_lu2",
+ 0x1000DF2: "Sinh_ruu2",
+ 0x1000DF3: "Sinh_luu2",
+ 0x1000DF4: "Sinh_kunddaliya",
+ };
+
+ /**
+ * All keysyms which should not repeat when held down.
+ * @private
+ */
+ var no_repeat = {
+ 0xFE03: true, // ISO Level 3 Shift (AltGr)
+ 0xFFE1: true, // Left shift
+ 0xFFE2: true, // Right shift
+ 0xFFE3: true, // Left ctrl
+ 0xFFE4: true, // Right ctrl
+ 0xFFE7: true, // Left meta
+ 0xFFE8: true, // Right meta
+ 0xFFE9: true, // Left alt
+ 0xFFEA: true, // Right alt
+ 0xFFEB: true, // Left hyper
+ 0xFFEC: true // Right hyper
+ };
+
+ /**
+ * All modifiers and their states.
+ */
+ this.modifiers = new Keyboard.ModifierState();
+
+ /**
+ * The state of every key, indexed by keysym. If a particular key is
+ * pressed, the value of pressed for that keysym will be true. If a key
+ * is not currently pressed, it will not be defined.
+ */
+ this.pressed = {};
+
+ /**
+ * The last result of calling the onkeydown handler for each key, indexed
+ * by keysym. This is used to prevent/allow default actions for key events,
+ * even when the onkeydown handler cannot be called again because the key
+ * is (theoretically) still pressed.
+ *
+ * @private
+ */
+ var last_keydown_result = {};
+
+ /**
+ * The keysym most recently associated with a given keycode when keydown
+ * fired. This object maps keycodes to keysyms.
+ *
+ * @private
+ * @type {Object.<Number, Number>}
+ */
+ var recentKeysym = {};
+
+ /**
+ * Timeout before key repeat starts.
+ * @private
+ */
+ var key_repeat_timeout = null;
+
+ /**
+ * Interval which presses and releases the last key pressed while that
+ * key is still being held down.
+ * @private
+ */
+ var key_repeat_interval = null;
+
+ /**
+ * Given an array of keysyms indexed by location, returns the keysym
+ * for the given location, or the keysym for the standard location if
+ * undefined.
+ *
+ * @private
+ * @param {Number[]} keysyms
+ * An array of keysyms, where the index of the keysym in the array is
+ * the location value.
+ *
+ * @param {Number} location
+ * The location on the keyboard corresponding to the key pressed, as
+ * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ */
+ var get_keysym = function get_keysym(keysyms, location) {
+
+ if (!keysyms)
+ return null;
+
+ return keysyms[location] || keysyms[0];
+ };
+
+ /**
+ * Returns true if the given keysym corresponds to a printable character,
+ * false otherwise.
+ *
+ * @param {Number} keysym
+ * The keysym to check.
+ *
+ * @returns {Boolean}
+ * true if the given keysym corresponds to a printable character,
+ * false otherwise.
+ */
+ var isPrintable = function isPrintable(keysym) {
+
+ // Keysyms with Unicode equivalents are printable
+ return (keysym >= 0x00 && keysym <= 0xFF)
+ || (keysym & 0xFFFF0000) === 0x01000000;
+
+ };
+
+ function keysym_from_key_identifier(identifier, location, shifted) {
+
+ if (!identifier)
+ return null;
+
+ var typedCharacter;
+
+ // If identifier is U+xxxx, decode Unicode character
+ var unicodePrefixLocation = identifier.indexOf("U+");
+ if (unicodePrefixLocation >= 0) {
+ var hex = identifier.substring(unicodePrefixLocation+2);
+ typedCharacter = String.fromCharCode(parseInt(hex, 16));
+ }
+
+ // If single character and not keypad, use that as typed character
+ else if (identifier.length === 1 && location !== 3)
+ typedCharacter = identifier;
+
+ // Otherwise, look up corresponding keysym
+ else
+ return get_keysym(keyidentifier_keysym[identifier], location);
+
+ // Alter case if necessary
+ if (shifted === true)
+ typedCharacter = typedCharacter.toUpperCase();
+ else if (shifted === false)
+ typedCharacter = typedCharacter.toLowerCase();
+
+ // Get codepoint
+ var codepoint = typedCharacter.charCodeAt(0);
+ return keysym_from_charcode(codepoint);
+
+ }
+
+ function isControlCharacter(codepoint) {
+ return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
+ }
+
+ function keysym_from_charcode(codepoint) {
+
+ // Keysyms for control characters
+ if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
+
+ // Keysyms for ASCII chars
+ if (codepoint >= 0x0000 && codepoint <= 0x00FF)
+ return codepoint;
+
+ // Keysyms for Unicode
+ if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
+ return 0x01000000 | codepoint;
+
+ return null;
+
+ }
+
+ function keysym_from_keycode(keyCode, location) {
+ return get_keysym(keycodeKeysyms[keyCode], location);
+ }
+
+ /**
+ * Heuristically detects if the legacy keyIdentifier property of
+ * a keydown/keyup event looks incorrectly derived. Chrome, and
+ * presumably others, will produce the keyIdentifier by assuming
+ * the keyCode is the Unicode codepoint for that key. This is not
+ * correct in all cases.
+ *
+ * @private
+ * @param {Number} keyCode
+ * The keyCode from a browser keydown/keyup event.
+ *
+ * @param {String} keyIdentifier
+ * The legacy keyIdentifier from a browser keydown/keyup event.
+ *
+ * @returns {Boolean}
+ * true if the keyIdentifier looks sane, false if the keyIdentifier
+ * appears incorrectly derived or is missing entirely.
+ */
+ var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {
+
+ // Missing identifier is not sane
+ if (!keyIdentifier)
+ return false;
+
+ // Assume non-Unicode keyIdentifier values are sane
+ var unicodePrefixLocation = keyIdentifier.indexOf("U+");
+ if (unicodePrefixLocation === -1)
+ return true;
+
+ // If the Unicode codepoint isn't identical to the keyCode,
+ // then the identifier is likely correct
+ var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
+ if (keyCode !== codepoint)
+ return true;
+
+ // The keyCodes for A-Z and 0-9 are actually identical to their
+ // Unicode codepoints
+ if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
+ return true;
+
+ // The keyIdentifier does NOT appear sane
+ return false;
+
+ };
+
+ /**
+ * Marks a key as pressed, firing the keydown event if registered. Key
+ * repeat for the pressed key will start after a delay if that key is
+ * not a modifier. The return value of this function depends on the
+ * return value of the keydown event handler, if any.
+ *
+ * @param {Number} keysym The keysym of the key to press.
+ * @return {Boolean} true if event should NOT be canceled, false otherwise.
+ */
+ this.press = function(keysym) {
+
+ // Don't bother with pressing the key if the key is unknown
+ if (keysym === null) return;
+
+ // Only press if released
+ if (!guac_keyboard.pressed[keysym]) {
+
+ // Mark key as pressed
+ guac_keyboard.pressed[keysym] = true;
+
+ // Send key event
+ if (guac_keyboard.onkeydown) {
+
+ var result = guac_keyboard.onkeydown(keysym_to_string[keysym],
+ modifier_state_to_str());
+ last_keydown_result[keysym] = result;
+
+ // Stop any current repeat
+ window.clearTimeout(key_repeat_timeout);
+ window.clearInterval(key_repeat_interval);
+
+ // Repeat after a delay as long as pressed
+ if (!no_repeat[keysym])
+ key_repeat_timeout = window.setTimeout(function() {
+ key_repeat_interval = window.setInterval(function() {
+ var mods = modifier_state_to_str();
+ guac_keyboard.onkeyup(keysym_to_string[keysym], mods);
+ guac_keyboard.onkeydown(keysym_to_string[keysym], mods);
+ }, 50);
+ }, 500);
+
+ return result;
+ }
+ }
+
+ // Return the last keydown result by default, resort to false if unknown
+ return last_keydown_result[keysym] || false;
+
+ };
+
+ /**
+ * Marks a key as released, firing the keyup event if registered.
+ *
+ * @param {Number} keysym The keysym of the key to release.
+ */
+ this.release = function(keysym) {
+
+ // Only release if pressed
+ if (guac_keyboard.pressed[keysym]) {
+
+ // Mark key as released
+ delete guac_keyboard.pressed[keysym];
+
+ // Stop repeat
+ window.clearTimeout(key_repeat_timeout);
+ window.clearInterval(key_repeat_interval);
+
+ // Send key event
+ if (keysym !== null && guac_keyboard.onkeyup)
+ guac_keyboard.onkeyup(keysym_to_string[keysym],
+ modifier_state_to_str());
+
+ }
+
+ };
+
+ /**
+ * Resets the state of this keyboard, releasing all keys, and firing keyup
+ * events for each released key.
+ */
+ this.reset = function() {
+
+ // Release all pressed keys
+ for (var keysym in guac_keyboard.pressed)
+ guac_keyboard.release(parseInt(keysym));
+
+ // Clear event log
+ eventLog = [];
+
+ };
+
+ /**
+ * Given a keyboard event, updates the local modifier state and remote
+ * key state based on the modifier flags within the event. This function
+ * pays no attention to keycodes.
+ *
+ * @private
+ * @param {KeyboardEvent} e
+ * The keyboard event containing the flags to update.
+ */
+ var update_modifier_state = function update_modifier_state(e) {
+
+ // Get state
+ var state = Keyboard.ModifierState.fromKeyboardEvent(e);
+
+ // Release alt if implicitly released
+ if (guac_keyboard.modifiers.alt && state.alt === false) {
+ guac_keyboard.release(0xFFE9); // Left alt
+ guac_keyboard.release(0xFFEA); // Right alt
+ guac_keyboard.release(0xFE03); // AltGr
+ }
+
+ // Release shift if implicitly released
+ if (guac_keyboard.modifiers.shift && state.shift === false) {
+ guac_keyboard.release(0xFFE1); // Left shift
+ guac_keyboard.release(0xFFE2); // Right shift
+ }
+
+ // Release ctrl if implicitly released
+ if (guac_keyboard.modifiers.ctrl && state.ctrl === false) {
+ guac_keyboard.release(0xFFE3); // Left ctrl
+ guac_keyboard.release(0xFFE4); // Right ctrl
+ }
+
+ // Release meta if implicitly released
+ if (guac_keyboard.modifiers.meta && state.meta === false) {
+ guac_keyboard.release(0xFFE7); // Left meta
+ guac_keyboard.release(0xFFE8); // Right meta
+ }
+
+ // Release hyper if implicitly released
+ if (guac_keyboard.modifiers.hyper && state.hyper === false) {
+ guac_keyboard.release(0xFFEB); // Left hyper
+ guac_keyboard.release(0xFFEC); // Right hyper
+ }
+
+ // Update state
+ guac_keyboard.modifiers = state;
+
+ };
+
+ /**
+ * Constructs a string representing all currently pressed modifiers.
+ *
+ * @return {String} The resulting string.
+ */
+ var modifier_state_to_str = function modifier_state_to_str() {
+ let masks = []
+ if (guac_keyboard.modifiers.alt) masks.push("alt-mask");
+ if (guac_keyboard.modifiers.ctrl) masks.push("control-mask");
+ if (guac_keyboard.modifiers.meta) masks.push("meta-mask");
+ if (guac_keyboard.modifiers.shift) masks.push("shift-mask");
+ if (guac_keyboard.modifiers.hyper) masks.push("hyper-mask");
+ return masks.join('+')
+ }
+
+ /**
+ * Reads through the event log, removing events from the head of the log
+ * when the corresponding true key presses are known (or as known as they
+ * can be).
+ *
+ * @private
+ * @return {Boolean} Whether the default action of the latest event should
+ * be prevented.
+ */
+ function interpret_events() {
+
+ // Do not prevent default if no event could be interpreted
+ var handled_event = interpret_event();
+ if (!handled_event)
+ return false;
+
+ // Interpret as much as possible
+ var last_event;
+ do {
+ last_event = handled_event;
+ handled_event = interpret_event();
+ } while (handled_event !== null);
+
+ return last_event.defaultPrevented;
+
+ }
+
+ /**
+ * Releases Ctrl+Alt, if both are currently pressed and the given keysym
+ * looks like a key that may require AltGr.
+ *
+ * @private
+ * @param {Number} keysym The key that was just pressed.
+ */
+ var release_simulated_altgr = function release_simulated_altgr(keysym) {
+
+ // Both Ctrl+Alt must be pressed if simulated AltGr is in use
+ if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt)
+ return;
+
+ // Assume [A-Z] never require AltGr
+ if (keysym >= 0x0041 && keysym <= 0x005A)
+ return;
+
+ // Assume [a-z] never require AltGr
+ if (keysym >= 0x0061 && keysym <= 0x007A)
+ return;
+
+ // Release Ctrl+Alt if the keysym is printable
+ if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) {
+ guac_keyboard.release(0xFFE3); // Left ctrl
+ guac_keyboard.release(0xFFE4); // Right ctrl
+ guac_keyboard.release(0xFFE9); // Left alt
+ guac_keyboard.release(0xFFEA); // Right alt
+ }
+
+ };
+
+ /**
+ * Reads through the event log, interpreting the first event, if possible,
+ * and returning that event. If no events can be interpreted, due to a
+ * total lack of events or the need for more events, null is returned. Any
+ * interpreted events are automatically removed from the log.
+ *
+ * @private
+ * @return {KeyEvent}
+ * The first key event in the log, if it can be interpreted, or null
+ * otherwise.
+ */
+ var interpret_event = function interpret_event() {
+
+ // Peek at first event in log
+ var first = eventLog[0];
+ if (!first)
+ return null;
+
+ // Keydown event
+ if (first instanceof KeydownEvent) {
+
+ var keysym = null;
+ var accepted_events = [];
+
+ // If event itself is reliable, no need to wait for other events
+ if (first.reliable) {
+ keysym = first.keysym;
+ accepted_events = eventLog.splice(0, 1);
+ }
+
+ // If keydown is immediately followed by a keypress, use the indicated character
+ else if (eventLog[1] instanceof KeypressEvent) {
+ keysym = eventLog[1].keysym;
+ accepted_events = eventLog.splice(0, 2);
+ }
+
+ // If keydown is immediately followed by anything else, then no
+ // keypress can possibly occur to clarify this event, and we must
+ // handle it now
+ else if (eventLog[1]) {
+ keysym = first.keysym;
+ accepted_events = eventLog.splice(0, 1);
+ }
+
+ // Fire a key press if valid events were found
+ if (accepted_events.length > 0) {
+
+ if (keysym) {
+
+ // Fire event
+ release_simulated_altgr(keysym);
+ var defaultPrevented = !guac_keyboard.press(keysym);
+ recentKeysym[first.keyCode] = keysym;
+
+ // If a key is pressed while meta is held down, the keyup will
+ // never be sent in Chrome, so send it now. (bug #108404)
+ if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8)
+ guac_keyboard.release(keysym);
+
+ // Record whether default was prevented
+ for (var i=0; i<accepted_events.length; i++)
+ accepted_events[i].defaultPrevented = defaultPrevented;
+
+ }
+
+ return first;
+
+ }
+
+ } // end if keydown
+
+ // Keyup event
+ else if (first instanceof KeyupEvent) {
+
+ // Release specific key if known
+ var keysym = first.keysym;
+ if (keysym) {
+ guac_keyboard.release(keysym);
+ first.defaultPrevented = true;
+ }
+
+ // Otherwise, fall back to releasing all keys
+ else {
+ guac_keyboard.reset();
+ return first;
+ }
+
+ return eventLog.shift();
+
+ } // end if keyup
+
+ // Ignore any other type of event (keypress by itself is invalid)
+ else
+ return eventLog.shift();
+
+ // No event interpreted
+ return null;
+
+ };
+
+ /**
+ * Returns the keyboard location of the key associated with the given
+ * keyboard event. The location differentiates key events which otherwise
+ * have the same keycode, such as left shift vs. right shift.
+ *
+ * @private
+ * @param {KeyboardEvent} e
+ * A JavaScript keyboard event, as received through the DOM via a
+ * "keydown", "keyup", or "keypress" handler.
+ *
+ * @returns {Number}
+ * The location of the key event on the keyboard, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ */
+ var getEventLocation = function getEventLocation(e) {
+
+ // Use standard location, if possible
+ if ('location' in e)
+ return e.location;
+
+ // Failing that, attempt to use deprecated keyLocation
+ if ('keyLocation' in e)
+ return e.keyLocation;
+
+ // If no location is available, assume left side
+ return 0;
+
+ };
+
+ // When key pressed
+ element.addEventListener("keydown", function(e) {
+
+ // Only intercept if handler set
+ if (!guac_keyboard.onkeydown) return;
+
+ var keyCode;
+ if (window.event) keyCode = window.event.keyCode;
+ else if (e.which) keyCode = e.which;
+
+ // Fix modifier states
+ update_modifier_state(e);
+
+ // Ignore (but do not prevent) the "composition" keycode sent by some
+ // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
+ if (keyCode === 229)
+ return;
+
+ // Log event
+ var keydownEvent = new KeydownEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
+ eventLog.push(keydownEvent);
+
+ // Interpret as many events as possible, prevent default if indicated
+ if (interpret_events())
+ e.preventDefault();
+
+ }, true);
+
+ // When key pressed
+ element.addEventListener("keypress", function(e) {
+
+ // Only intercept if handler set
+ if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
+
+ var charCode;
+ if (window.event) charCode = window.event.keyCode;
+ else if (e.which) charCode = e.which;
+
+ // Fix modifier states
+ update_modifier_state(e);
+
+ // Log event
+ var keypressEvent = new KeypressEvent(charCode);
+ eventLog.push(keypressEvent);
+
+ // Interpret as many events as possible, prevent default if indicated
+ if (interpret_events())
+ e.preventDefault();
+
+ }, true);
+
+ // When key released
+ element.addEventListener("keyup", function(e) {
+
+ // Only intercept if handler set
+ if (!guac_keyboard.onkeyup) return;
+
+ e.preventDefault();
+
+ var keyCode;
+ if (window.event) keyCode = window.event.keyCode;
+ else if (e.which) keyCode = e.which;
+
+ // Fix modifier states
+ update_modifier_state(e);
+
+ // Log event, call for interpretation
+ var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
+ eventLog.push(keyupEvent);
+ interpret_events();
+
+ }, true);
+
+};
+
+/**
+ * The state of all supported keyboard modifiers.
+ * @constructor
+ */
+Keyboard.ModifierState = function() {
+
+ /**
+ * Whether shift is currently pressed.
+ * @type {Boolean}
+ */
+ this.shift = false;
+
+ /**
+ * Whether ctrl is currently pressed.
+ * @type {Boolean}
+ */
+ this.ctrl = false;
+
+ /**
+ * Whether alt is currently pressed.
+ * @type {Boolean}
+ */
+ this.alt = false;
+
+ /**
+ * Whether meta (apple key) is currently pressed.
+ * @type {Boolean}
+ */
+ this.meta = false;
+
+ /**
+ * Whether hyper (windows key) is currently pressed.
+ * @type {Boolean}
+ */
+ this.hyper = false;
+
+};
+
+/**
+ * Returns the modifier state applicable to the keyboard event given.
+ *
+ * @param {KeyboardEvent} e The keyboard event to read.
+ * @returns {Keyboard.ModifierState} The current state of keyboard
+ * modifiers.
+ */
+Keyboard.ModifierState.fromKeyboardEvent = function(e) {
+
+ var state = new Keyboard.ModifierState();
+
+ // Assign states from old flags
+ state.shift = e.shiftKey;
+ state.ctrl = e.ctrlKey;
+ state.alt = e.altKey;
+ state.meta = e.metaKey;
+
+ // Use DOM3 getModifierState() for others
+ if (e.getModifierState) {
+ state.hyper = e.getModifierState("OS")
+ || e.getModifierState("Super")
+ || e.getModifierState("Hyper")
+ || e.getModifierState("Win");
+ }
+
+ return state;
+
+};
diff --git a/net/webrtc/www/theme.css b/net/webrtc/www/theme.css
new file mode 100644
index 00000000..a79dc14a
--- /dev/null
+++ b/net/webrtc/www/theme.css
@@ -0,0 +1,141 @@
+/* Reset CSS from Eric Meyer */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/* Our style */
+
+body{
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background-color: #222;
+ color: white;
+}
+
+.holygrail-body {
+ flex: 1 0 auto;
+ display: flex;
+}
+
+.holygrail-body .content {
+ width: 100%;
+}
+
+#sessions {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-around;
+}
+
+.holygrail-body .nav {
+ width: 220px;
+ list-style: none;
+ text-align: left;
+ order: -1;
+ background-color: #333;
+ margin: 0;
+}
+
+@media (max-width: 700px) {
+ .holygrail-body {
+ flex-direction: column;
+ }
+
+ .holygrail-body .nav {
+ width: 100%;
+ }
+}
+
+.session p span {
+ float: right;
+}
+
+.session p {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+.stream {
+ background-color: black;
+ width: 480px;
+}
+
+#camera-list {
+ text-align: center;
+}
+
+.button {
+ border: none;
+ padding: 8px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ -webkit-transition-duration: 0.4s; /* Safari */
+ transition-duration: 0.4s;
+ cursor: pointer;
+ margin: 5px auto;
+ width: 90%;
+}
+
+.button1 {
+ background-color: #222;
+ color: white;
+ border: 2px solid #4CAF50;
+ word-wrap: anywhere;
+}
+
+.button1:hover {
+ background-color: #4CAF50;
+ color: white;
+}
+
+#image-holder {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-around;
+}
diff --git a/net/webrtc/www/webrtc.js b/net/webrtc/www/webrtc.js
new file mode 100644
index 00000000..eb449d90
--- /dev/null
+++ b/net/webrtc/www/webrtc.js
@@ -0,0 +1,466 @@
+/* vim: set sts=4 sw=4 et :
+ *
+ * Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
+ * with a GStreamer app. Runs only in passive mode, i.e., responds to offers
+ * with answers, exchanges ICE candidates, and streams.
+ *
+ * Author: Nirbheek Chauhan <nirbheek@centricular.com>
+ */
+
+// Set this to override the automatic detection in websocketServerConnect()
+var ws_server;
+var ws_port;
+// Override with your own STUN servers if you want
+var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"},
+ /* TODO: do not keep these static and in clear text in production,
+ * and instead use one of the mechanisms discussed in
+ * https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA
+ */
+ {'urls': 'turn:turn.homeneural.net:3478?transport=udp',
+ 'credential': '1qaz2wsx',
+ 'username': 'test'
+ }],
+ /* Uncomment the following line to ensure the turn server is used
+ * while testing. This should be kept commented out in production,
+ * as non-relay ice candidates should be preferred
+ */
+ // iceTransportPolicy: "relay",
+ };
+
+var sessions = {}
+
+/* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */
+function getOurId() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+function Uint8ToString(u8a){
+ var CHUNK_SZ = 0x8000;
+ var c = [];
+ for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
+ c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
+ }
+ return c.join("");
+}
+
+function Session(our_id, peer_id, closed_callback) {
+ this.id = null;
+ this.peer_connection = null;
+ this.ws_conn = null;
+ this.peer_id = peer_id;
+ this.our_id = our_id;
+ this.closed_callback = closed_callback;
+ this.data_channel = null;
+ this.input = null;
+
+ this.getVideoElement = function() {
+ return document.getElementById("stream-" + this.our_id);
+ };
+
+ this.resetState = function() {
+ if (this.peer_connection) {
+ this.peer_connection.close();
+ this.peer_connection = null;
+ }
+ var videoElement = this.getVideoElement();
+ if (videoElement) {
+ videoElement.pause();
+ videoElement.src = "";
+ }
+
+ var session_div = document.getElementById("session-" + this.our_id);
+ if (session_div) {
+ session_div.parentNode.removeChild(session_div);
+ }
+ if (this.ws_conn) {
+ this.ws_conn.close();
+ this.ws_conn = null;
+ }
+
+ this.input && this.input.detach();
+ this.data_channel = null;
+ };
+
+ this.handleIncomingError = function(error) {
+ this.resetState();
+ this.closed_callback(this.our_id);
+ };
+
+ this.setStatus = function(text) {
+ console.log(text);
+ var span = document.getElementById("status-" + this.our_id);
+ // Don't set the status if it already contains an error
+ if (!span.classList.contains('error'))
+ span.textContent = text;
+ };
+
+ this.setError = function(text) {
+ console.error(text);
+ var span = document.getElementById("status-" + this.our_id);
+ span.textContent = text;
+ span.classList.add('error');
+ this.resetState();
+ this.closed_callback(this.our_id);
+ };
+
+ // Local description was set, send it to peer
+ this.onLocalDescription = function(desc) {
+ console.log("Got local description: " + JSON.stringify(desc), this);
+ var thiz = this;
+ this.peer_connection.setLocalDescription(desc).then(() => {
+ this.setStatus("Sending SDP answer");
+ var sdp = {
+ 'type': 'peer',
+ 'sessionId': this.id,
+ 'sdp': this.peer_connection.localDescription.toJSON()
+ };
+ this.ws_conn.send(JSON.stringify(sdp));
+ }).catch(function(e) {
+ thiz.setError(e);
+ });
+ };
+
+ this.onRemoteDescriptionSet = function() {
+ this.setStatus("Remote SDP set");
+ this.setStatus("Got SDP offer");
+ this.peer_connection.createAnswer()
+ .then(this.onLocalDescription.bind(this)).catch(this.setError);
+ }
+
+ // SDP offer received from peer, set remote description and create an answer
+ this.onIncomingSDP = function(sdp) {
+ var thiz = this;
+ this.peer_connection.setRemoteDescription(sdp)
+ .then(this.onRemoteDescriptionSet.bind(this))
+ .catch(function(e) {
+ thiz.setError(e)
+ });
+ };
+
+ // ICE candidate received from peer, add it to the peer connection
+ this.onIncomingICE = function(ice) {
+ var candidate = new RTCIceCandidate(ice);
+ var thiz = this;
+ this.peer_connection.addIceCandidate(candidate).catch(function(e) {
+ thiz.setError(e)
+ });
+ };
+
+ this.onServerMessage = function(event) {
+ console.log("Received " + event.data);
+ try {
+ msg = JSON.parse(event.data);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ this.handleIncomingError("Error parsing incoming JSON: " + event.data);
+ } else {
+ this.handleIncomingError("Unknown error parsing response: " + event.data);
+ }
+ return;
+ }
+
+ if (msg.type == "registered") {
+ this.setStatus("Registered with server");
+ this.connectPeer();
+ } else if (msg.type == "sessionStarted") {
+ this.setStatus("Registered with server");
+ this.id = msg.sessionId;
+ } else if (msg.type == "error") {
+ this.handleIncomingError(msg.details);
+ } else if (msg.type == "endSession") {
+ this.resetState();
+ this.closed_callback(this.our_id);
+ } else if (msg.type == "peer") {
+ // Incoming peer message signals the beginning of a call
+ if (!this.peer_connection)
+ this.createCall(msg);
+
+ if (msg.sdp != null) {
+ this.onIncomingSDP(msg.sdp);
+ } else if (msg.ice != null) {
+ this.onIncomingICE(msg.ice);
+ } else {
+ this.handleIncomingError("Unknown incoming JSON: " + msg);
+ }
+ }
+ };
+
+ this.streamIsPlaying = function(e) {
+ this.setStatus("Streaming");
+ };
+
+ this.onServerClose = function(event) {
+ this.resetState();
+ this.closed_callback(this.our_id);
+ };
+
+ this.onServerError = function(event) {
+ this.handleIncomingError('Server error');
+ };
+
+ this.websocketServerConnect = function() {
+ // Clear errors in the status span
+ var span = document.getElementById("status-" + this.our_id);
+ span.classList.remove('error');
+ span.textContent = '';
+ console.log("Our ID:", this.our_id);
+ var ws_port = ws_port || '8443';
+ if (window.location.protocol.startsWith ("file")) {
+ var ws_server = ws_server || "127.0.0.1";
+ } else if (window.location.protocol.startsWith ("http")) {
+ var ws_server = ws_server || window.location.hostname;
+ } else {
+ throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
+ }
+ var ws_url = 'ws://' + ws_server + ':' + ws_port
+ this.setStatus("Connecting to server " + ws_url);
+ this.ws_conn = new WebSocket(ws_url);
+ /* When connected, immediately register with the server */
+ this.ws_conn.addEventListener('open', (event) => {
+ this.setStatus("Connecting to the peer");
+ this.connectPeer();
+ });
+ this.ws_conn.addEventListener('error', this.onServerError.bind(this));
+ this.ws_conn.addEventListener('message', this.onServerMessage.bind(this));
+ this.ws_conn.addEventListener('close', this.onServerClose.bind(this));
+ };
+
+ this.connectPeer = function() {
+ this.setStatus("Connecting " + this.peer_id);
+
+ this.ws_conn.send(JSON.stringify({
+ "type": "startSession",
+ "peerId": this.peer_id
+ }));
+ };
+
+ this.onRemoteStreamAdded = function(event) {
+ var videoTracks = event.stream.getVideoTracks();
+ var audioTracks = event.stream.getAudioTracks();
+
+ console.log(videoTracks);
+
+ if (videoTracks.length > 0) {
+ console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
+ this.getVideoElement().srcObject = event.stream;
+ this.getVideoElement().play();
+ } else {
+ this.handleIncomingError('Stream with unknown tracks added, resetting');
+ }
+ };
+
+ this.createCall = function(msg) {
+ console.log('Creating RTCPeerConnection');
+
+ this.peer_connection = new RTCPeerConnection(rtc_configuration);
+ this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this);
+
+ this.peer_connection.ondatachannel = (event) => {
+ console.log(`Data channel created: ${event.channel.label}`);
+ this.data_channel = event.channel;
+
+ video_element = this.getVideoElement();
+ if (video_element) {
+ this.input = new Input(video_element, (data) => {
+ if (this.data_channel) {
+ console.log(`Navigation data: ${data}`);
+ this.data_channel.send(JSON.stringify(data));
+ }
+ });
+ }
+
+ this.data_channel.onopen = (event) => {
+ console.log("Receive channel opened, attaching input");
+ this.input.attach();
+ }
+ this.data_channel.onclose = (event) => {
+ console.info("Receive channel closed");
+ this.input && this.input.detach();
+ this.data_channel = null;
+ }
+ this.data_channel.onerror = (event) => {
+ this.input && this.input.detach();
+ console.warn("Error on receive channel", event.data);
+ this.data_channel = null;
+ }
+
+ let buffer = [];
+ this.data_channel.onmessage = (event) => {
+ if (typeof event.data === 'string' || event.data instanceof String) {
+ if (event.data == 'BEGIN_IMAGE')
+ buffer = [];
+ else if (event.data == 'END_IMAGE') {
+ var decoder = new TextDecoder("ascii");
+ var array_buffer = new Uint8Array(buffer);
+ var str = decoder.decode(array_buffer);
+ let img = document.getElementById("image");
+ img.src = 'data:image/png;base64, ' + str;
+ }
+ } else {
+ var i, len = buffer.length
+ var view = new DataView(event.data);
+ for (i = 0; i < view.byteLength; i++) {
+ buffer[len + i] = view.getUint8(i);
+ }
+ }
+ }
+ }
+
+ this.peer_connection.onicecandidate = (event) => {
+ if (event.candidate == null) {
+ console.log("ICE Candidate was null, done");
+ return;
+ }
+ this.ws_conn.send(JSON.stringify({
+ "type": "peer",
+ "sessionId": this.id,
+ "ice": event.candidate.toJSON()
+ }));
+ };
+
+ this.setStatus("Created peer connection for call, waiting for SDP");
+ };
+
+ document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false);
+
+ this.websocketServerConnect();
+}
+
+function startSession() {
+ var peer_id = document.getElementById("camera-id").value;
+
+ if (peer_id === "") {
+ return;
+ }
+
+ sessions[peer_id] = new Session(peer_id);
+}
+
+function session_closed(peer_id) {
+ sessions[peer_id] = null;
+}
+
+function addPeer(peer_id, meta) {
+ console.log("Meta: ", JSON.stringify(meta));
+
+ var nav_ul = document.getElementById("camera-list");
+
+ meta = meta ? meta : {"display-name": peer_id};
+ let display_html = `${meta["display-name"] ? meta["display-name"] : peer_id}<ul>`;
+ for (const key in meta) {
+ if (key != "display-name") {
+ display_html += `<li>- ${key}: ${meta[key]}</li>`;
+ }
+ }
+ display_html += "</ul>"
+
+ var li_str = '<li id="peer-' + peer_id + '"><button class="button button1">' + display_html + '</button></li>';
+
+ nav_ul.insertAdjacentHTML('beforeend', li_str);
+ var li = document.getElementById("peer-" + peer_id);
+ li.onclick = function(e) {
+ var sessions_div = document.getElementById('sessions');
+ var our_id = getOurId();
+ var session_div_str = '<div class="session" id="session-' + our_id + '"><video preload="none" class="stream" id="stream-' + our_id + '"></video><p>Status: <span id="status-' + our_id + '">unknown</span></p></div>'
+ sessions_div.insertAdjacentHTML('beforeend', session_div_str);
+ sessions[peer_id] = new Session(our_id, peer_id, session_closed);
+ }
+}
+
+function clearPeers() {
+ var nav_ul = document.getElementById("camera-list");
+
+ while (nav_ul.firstChild) {
+ nav_ul.removeChild(nav_ul.firstChild);
+ }
+}
+
+function onServerMessage(event) {
+ console.log("Received " + event.data);
+
+ try {
+ msg = JSON.parse(event.data);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ console.error("Error parsing incoming JSON: " + event.data);
+ } else {
+ console.error("Unknown error parsing response: " + event.data);
+ }
+ return;
+ }
+
+ if (msg.type == "welcome") {
+ console.info(`Got welcomed with ID ${msg.peer_id}`);
+ ws_conn.send(JSON.stringify({
+ "type": "list"
+ }));
+ } else if (msg.type == "list") {
+ clearPeers();
+ for (i = 0; i < msg.producers.length; i++) {
+ addPeer(msg.producers[i].id, msg.producers[i].meta);
+ }
+ } else if (msg.type == "peerStatusChanged") {
+ var li = document.getElementById("peer-" + msg.peerId);
+ if (msg.roles.includes("producer")) {
+ if (li == null) {
+ console.error('Adding peer');
+ addPeer(msg.peerId, msg.meta);
+ }
+ } else if (li != null) {
+ li.parentNode.removeChild(li);
+ }
+ } else {
+ console.error("Unsupported message: ", msg);
+ }
+};
+
+function clearConnection() {
+ ws_conn.removeEventListener('error', onServerError);
+ ws_conn.removeEventListener('message', onServerMessage);
+ ws_conn.removeEventListener('close', onServerClose);
+ ws_conn = null;
+}
+
+function onServerClose(event) {
+ clearConnection();
+ clearPeers();
+ console.log("Close");
+ window.setTimeout(connect, 1000);
+};
+
+function onServerError(event) {
+ clearConnection();
+ clearPeers();
+ console.log("Error", event);
+ window.setTimeout(connect, 1000);
+};
+
+function connect() {
+ var ws_port = ws_port || '8443';
+ if (window.location.protocol.startsWith ("file")) {
+ var ws_server = ws_server || "127.0.0.1";
+ } else if (window.location.protocol.startsWith ("http")) {
+ var ws_server = ws_server || window.location.hostname;
+ } else {
+ throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
+ }
+ var ws_url = 'ws://' + ws_server + ':' + ws_port
+ console.log("Connecting listener");
+ ws_conn = new WebSocket(ws_url);
+ ws_conn.addEventListener('open', (event) => {
+ ws_conn.send(JSON.stringify({
+ "type": "setPeerStatus",
+ "roles": ["listener"]
+ }));
+ });
+ ws_conn.addEventListener('error', onServerError);
+ ws_conn.addEventListener('message', onServerMessage);
+ ws_conn.addEventListener('close', onServerClose);
+}
+
+function setup() {
+ connect();
+}