Add support for multiple origins (#82)
Adds `moq-api` to get/set the origin for each broadcast. Not used by default for local development.
This commit is contained in:
parent
5e4eb420c0
commit
04ff9d5a6a
|
@ -8,3 +8,10 @@ insert_final_newline = true
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
|
@ -83,6 +83,12 @@ dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
@ -103,7 +109,7 @@ dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"fastrand",
|
"fastrand 1.9.0",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
@ -137,7 +143,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"parking",
|
"parking",
|
||||||
"polling",
|
"polling",
|
||||||
"rustix",
|
"rustix 0.37.23",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2 0.4.9",
|
"socket2 0.4.9",
|
||||||
"waker-fn",
|
"waker-fn",
|
||||||
|
@ -184,6 +190,17 @@ version = "4.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
|
checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -207,6 +224,55 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.6.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"mime",
|
||||||
|
"rustversion",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
|
@ -240,6 +306,12 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -259,7 +331,7 @@ dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-task",
|
"async-task",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"fastrand",
|
"fastrand 1.9.0",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
@ -353,6 +425,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -480,12 +566,33 @@ dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -555,7 +662,7 @@ version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand 1.9.0",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -790,6 +897,33 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"rustls 0.21.7",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"hyper",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -840,6 +974,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -882,6 +1022,12 @@ version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
@ -901,6 +1047,12 @@ dependencies = [
|
||||||
"value-bag",
|
"value-bag",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.3"
|
version = "2.6.3"
|
||||||
|
@ -943,6 +1095,25 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moq-api"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"hyper",
|
||||||
|
"log",
|
||||||
|
"redis",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moq-pub"
|
name = "moq-pub"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -951,7 +1122,6 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"http",
|
|
||||||
"log",
|
"log",
|
||||||
"moq-transport",
|
"moq-transport",
|
||||||
"mp4",
|
"mp4",
|
||||||
|
@ -963,7 +1133,7 @@ dependencies = [
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"webtransport-generic",
|
"url",
|
||||||
"webtransport-quinn",
|
"webtransport-quinn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -976,16 +1146,18 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
|
"moq-api",
|
||||||
"moq-transport",
|
"moq-transport",
|
||||||
"quinn",
|
"quinn",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls 0.21.7",
|
"rustls 0.21.7",
|
||||||
|
"rustls-native-certs",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"url",
|
||||||
"warp",
|
"warp",
|
||||||
"webtransport-generic",
|
|
||||||
"webtransport-quinn",
|
"webtransport-quinn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -993,7 +1165,6 @@ dependencies = [
|
||||||
name = "moq-transport"
|
name = "moq-transport"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1051,13 +1222,21 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "native-tls"
|
||||||
version = "0.46.0"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"overload",
|
"lazy_static",
|
||||||
"winapi",
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1128,6 +1307,32 @@ version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -1135,10 +1340,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "openssl-sys"
|
||||||
version = "0.1.1"
|
version = "0.9.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
|
@ -1207,6 +1418,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
@ -1214,7 +1431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1325,13 +1542,40 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "0.23.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"combine",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"itoa",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustls 0.21.7",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"ryu",
|
||||||
|
"sha1_smol",
|
||||||
|
"socket2 0.4.9",
|
||||||
|
"tokio",
|
||||||
|
"tokio-retry",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1363,6 +1607,49 @@ version = "0.7.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.11.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.4",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
|
"ipnet",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustls 0.21.7",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"system-configuration",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfc6381-codec"
|
name = "rfc6381-codec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1413,11 +1700,24 @@ version = "0.37.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
|
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"errno",
|
"errno",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys 0.3.8",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.0",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys 0.4.10",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1476,6 +1776,12 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -1519,7 +1825,7 @@ version = "2.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1567,6 +1873,16 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1591,13 +1907,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sha1_smol"
|
||||||
version = "0.1.4"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
|
@ -1672,6 +1985,46 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fastrand 2.0.1",
|
||||||
|
"redox_syscall",
|
||||||
|
"rustix 0.38.13",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -1701,16 +2054,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thread_local"
|
|
||||||
version = "1.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -1756,6 +2099,27 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-native-tls"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||||
|
dependencies = [
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-retry"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project",
|
||||||
|
"rand",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.23.4"
|
version = "0.23.4"
|
||||||
|
@ -1767,6 +2131,16 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||||
|
dependencies = [
|
||||||
|
"rustls 0.21.7",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
|
@ -1804,6 +2178,28 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-service"
|
name = "tower-service"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -1841,32 +2237,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"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.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
|
||||||
dependencies = [
|
|
||||||
"nu-ansi-term",
|
|
||||||
"sharded-slab",
|
|
||||||
"smallvec",
|
|
||||||
"thread_local",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-log",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1945,6 +2315,7 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1959,18 +2330,18 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "valuable"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag"
|
name = "value-bag"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
|
checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -2016,7 +2387,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.23.4",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -2116,6 +2487,12 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webtransport-generic"
|
name = "webtransport-generic"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -2128,20 +2505,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webtransport-proto"
|
name = "webtransport-proto"
|
||||||
version = "0.5.4"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54d41127a79f4d34112114b626f71d197c3ddf4fc82d56ccddc03a851bd0ea4f"
|
checksum = "ebeada5037d6302980ae2e0ab8d840e329c1697c612c6c077172de2b7631a276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webtransport-quinn"
|
name = "webtransport-quinn"
|
||||||
version = "0.5.4"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a7cccdcf10a2fb3a18ebd51fb8734e385624cb04fde38b239dbda0f1e40ba21"
|
checksum = "cceb876dbd00a87b3fd8869d1c315e07c28b0eb54d59b592a07a634f5e2b64e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2151,6 +2529,7 @@ dependencies = [
|
||||||
"quinn-proto",
|
"quinn-proto",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"webtransport-generic",
|
"webtransport-generic",
|
||||||
"webtransport-proto",
|
"webtransport-proto",
|
||||||
]
|
]
|
||||||
|
@ -2251,3 +2630,13 @@ name = "windows_x86_64_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.50.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["moq-transport", "moq-relay", "moq-pub"]
|
members = ["moq-transport", "moq-relay", "moq-pub", "moq-api"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
77
README.md
77
README.md
|
@ -6,39 +6,17 @@ Media over QUIC (MoQ) is a live media delivery protocol utilizing QUIC streams.
|
||||||
See [quic.video](https://quic.video) for more information.
|
See [quic.video](https://quic.video) for more information.
|
||||||
|
|
||||||
This repository contains a few crates:
|
This repository contains a few crates:
|
||||||
- **moq-relay**: A relay server, accepting content from publishers and fanning it out to subscribers.
|
|
||||||
- **moq-pub**: A publish client, accepting media from stdin (ex. via ffmpeg) and sending it to a remote server.
|
|
||||||
- **moq-transport**: An async implementation of the underlying MoQ protocol.
|
|
||||||
|
|
||||||
There's currently no way to actually view content with `moq-rs`; you'll need to use [moq-js](https://github.com/kixelated/moq-js) for that.
|
- **moq-relay**: A relay server, accepting content from publishers and fanning it out to subscribers.
|
||||||
|
- **moq-pub**: A publish client, accepting media from stdin (ex. via ffmpeg) and sending it to a remote server.
|
||||||
|
- **moq-transport**: An async implementation of the underlying MoQ protocol.
|
||||||
|
- **moq-api**: A HTTP API server that stores the origin for each broadcast, backed by redis.
|
||||||
|
|
||||||
## Setup
|
There's currently no way to view media with `moq-rs`; you'll need to use [moq-js](https://github.com/kixelated/moq-js) for that.
|
||||||
|
|
||||||
### Certificates
|
## Development
|
||||||
|
|
||||||
Unfortunately, QUIC mandates TLS and makes local development difficult.
|
See the [dev/README.md] helper scripts for local development.
|
||||||
If you have a valid certificate you can use it instead of self-signing.
|
|
||||||
|
|
||||||
Use [mkcert](https://github.com/FiloSottile/mkcert) to generate a self-signed certificate.
|
|
||||||
Unfortunately, this currently requires Go in order to [fork](https://github.com/FiloSottile/mkcert/pull/513) the tool.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./dev/cert
|
|
||||||
```
|
|
||||||
|
|
||||||
Unfortunately, WebTransport in Chrome currently (May 2023) doesn't verify certificates using the root CA.
|
|
||||||
The workaround is to use the `serverFingerprints` options, which requires the certificate MUST be only valid for at most **14 days**.
|
|
||||||
This is also why we're using a fork of mkcert, because it generates certificates valid for years by default.
|
|
||||||
This limitation will be removed once Chrome uses the system CA for WebTransport.
|
|
||||||
|
|
||||||
### Media
|
|
||||||
|
|
||||||
If you're using `moq-pub` then you'll want some test footage to broadcast.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir media
|
|
||||||
wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O dev/source.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -46,53 +24,42 @@ wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBun
|
||||||
|
|
||||||
**moq-relay** is a server that forwards subscriptions from publishers to subscribers, caching and deduplicating along the way.
|
**moq-relay** is a server that forwards subscriptions from publishers to subscribers, caching and deduplicating along the way.
|
||||||
It's designed to be run in a datacenter, relaying media across multiple hops to deduplicate and improve QoS.
|
It's designed to be run in a datacenter, relaying media across multiple hops to deduplicate and improve QoS.
|
||||||
|
The relays register themselves via the [moq-api] endpoints, which is used to discover other relays and share broadcasts.
|
||||||
You can run the development server with the following command, automatically using the self-signed certificate generated earlier:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./dev/relay
|
|
||||||
```
|
|
||||||
|
|
||||||
Notable arguments:
|
Notable arguments:
|
||||||
|
|
||||||
- `--bind <ADDR>` Listen on this address [default: [::]:4443]
|
- `--listen <ADDR>` Listen on this address [default: [::]:4443]
|
||||||
- `--cert <CERT>` Use the certificate file at this path
|
- `--cert <CERT>` Use the certificate file at this path
|
||||||
- `--key <KEY>` Use the private key at this path
|
- `--key <KEY>` Use the private key at this path
|
||||||
|
- `--fingerprint` Listen via HTTPS as well, serving the `/fingerprint` of the self-signed certificate. (dev only)
|
||||||
|
|
||||||
This listens for WebTransport connections on `UDP https://localhost:4443` by default.
|
This listens for WebTransport connections on `UDP https://localhost:4443` by default.
|
||||||
You need a client to connect to that address, to both publish and consume media.
|
You need a client to connect to that address, to both publish and consume media.
|
||||||
|
|
||||||
The server also listens on `TCP localhost:4443` when in development mode.
|
|
||||||
This is exclusively to serve a `/fingerprint` endpoint via HTTPS for self-signed certificates, which are not needed in production.
|
|
||||||
|
|
||||||
### moq-pub
|
### moq-pub
|
||||||
|
|
||||||
This is a client that publishes a fMP4 stream from stdin over MoQ.
|
This is a client that publishes a fMP4 stream from stdin over MoQ.
|
||||||
This can be combined with ffmpeg (and other tools) to produce a live stream.
|
This can be combined with ffmpeg (and other tools) to produce a live stream.
|
||||||
|
|
||||||
The following command runs a development instance, broadcasing `dev/source.mp4` to `localhost:4443`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./dev/pub
|
|
||||||
```
|
|
||||||
|
|
||||||
Notable arguments:
|
Notable arguments:
|
||||||
|
|
||||||
- `<URI>` connect to the given address, which must start with moq://.
|
- `<URL>` connect to the given address, which must start with https:// for WebTransport.
|
||||||
|
|
||||||
### moq-js
|
**NOTE**: We're very particular about the fMP4 ingested. See [dev/pub] for the required ffmpeg flags.
|
||||||
|
|
||||||
There's currently no way to consume broadcasts with `moq-rs`, at least until somebody writes `moq-sub`.
|
### moq-transport
|
||||||
Until then, you can use [moq.js](https://github.com/kixelated/moq-js) both watch broadcasts and publish broadcasts.
|
|
||||||
|
|
||||||
There's a hosted version available at [quic.video](https://quic.video/).
|
A media-agnostic library used by [moq-relay] and [moq-pub] to serve the underlying subscriptions.
|
||||||
There's a secret `?server` parameter that can be used to connect to a different address.
|
It has caching/deduplication built-in, so your application is oblivious to the number of connections under the hood.
|
||||||
|
Somebody build a non-media application using this library and I'll link it here!
|
||||||
|
|
||||||
- Publish to localhost: `https://quic.video/publish/?server=localhost:4443`
|
See the published [crate](https://crates.io/crates/moq-transport) and [documentation](https://docs.rs/moq-transport/latest/moq_transport/).
|
||||||
- Watch from localhost: `https://quic.video/watch/<name>/?server=localhost:4443`
|
|
||||||
|
|
||||||
Note that self-signed certificates are ONLY supported if the server name starts with `localhost`.
|
### moq-api
|
||||||
You'll need to add an entry to `/etc/hosts` if you want to use a self-signed certs and an IP address.
|
|
||||||
|
This is a API server that exposes a REST API.
|
||||||
|
It's used by relays to inserts themselves as origins when publishing, and to find the origin when subscribing.
|
||||||
|
It's basically just a thin wrapper around redis.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
# dev
|
||||||
|
|
||||||
|
This is a collection of helpful scripts for local development ONLY.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### moq-relay
|
||||||
|
|
||||||
|
Unfortunately, QUIC mandates TLS and makes local development difficult.
|
||||||
|
If you have a valid certificate you can use it instead of self-signing.
|
||||||
|
|
||||||
|
Use [mkcert](https://github.com/FiloSottile/mkcert) to generate a self-signed certificate.
|
||||||
|
Unfortunately, this currently requires [Go](https://golang.org/) to be installed in order to [fork](https://github.com/FiloSottile/mkcert/pull/513) the tool.
|
||||||
|
Somebody should get that merged or make something similar in Rust...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/cert
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, WebTransport in Chrome currently (May 2023) doesn't verify certificates using the root CA.
|
||||||
|
The workaround is to use the `serverFingerprints` options, which requires the certificate MUST be only valid for at most **14 days**.
|
||||||
|
This is also why we're using a fork of mkcert, because it generates certificates valid for years by default.
|
||||||
|
This limitation will be removed once Chrome uses the system CA for WebTransport.
|
||||||
|
|
||||||
|
### moq-pub
|
||||||
|
|
||||||
|
You'll want some test footage to broadcast.
|
||||||
|
Anything works, but make sure the codec is supported by the player since `moq-pub` does not re-encode.
|
||||||
|
|
||||||
|
Here's a criticially acclaimed short film:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir media
|
||||||
|
wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O dev/source.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
`moq-pub` uses [ffmpeg](https://ffmpeg.org/) to convert the media to fMP4.
|
||||||
|
You should have it installed already if you're a video nerd, otherwise:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
### moq-api
|
||||||
|
|
||||||
|
`moq-api` uses a redis instance to store active origins for clustering.
|
||||||
|
This is not relevant for most local development and the code path is skipped by default.
|
||||||
|
|
||||||
|
However, if you want to test the clustering, you'll need either either [Docker](https://www.docker.com/) or [Podman](https://podman.io/) installed.
|
||||||
|
We run the redis instance via a container automatically as part of `dev/api`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
**tl;dr** run these commands in seperate terminals:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/cert
|
||||||
|
./dev/relay
|
||||||
|
./dev/pub
|
||||||
|
```
|
||||||
|
|
||||||
|
They will each print out a URL you can use to publish/watch broadcasts.
|
||||||
|
|
||||||
|
### moq-relay
|
||||||
|
|
||||||
|
You can run the relay with the following command, automatically using the self-signed certificates generated earlier.
|
||||||
|
This listens for WebTransport connections on WebTransport `https://localhost:4443` by default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/relay
|
||||||
|
```
|
||||||
|
|
||||||
|
### moq-pub
|
||||||
|
|
||||||
|
The following command runs a development instance, broadcasing `dev/source.mp4` to WebTransport `https://localhost:4443`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/pub
|
||||||
|
```
|
||||||
|
|
||||||
|
### moq-api
|
||||||
|
|
||||||
|
The following commands runs an API server, listening for HTTP requests on `http://localhost:4442` by default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/api
|
||||||
|
```
|
||||||
|
|
||||||
|
Nodes can now register themselves via the API, which means you can run multiple interconnected relays.
|
||||||
|
There's two separate `dev/relay-0` and `dev/relay-1` scripts to test clustering locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev/relay-0
|
||||||
|
./dev/relay-1
|
||||||
|
```
|
||||||
|
|
||||||
|
These listen on `:4443` and `:4444` respectively, inserting themselves into the origin database as `localhost:$PORT`.
|
||||||
|
|
||||||
|
There's also a separate `dev/pub-1` script to publish to the `:4444` instance.
|
||||||
|
You can use the exisitng `dev/pub` script to publish to the `:4443` instance.
|
||||||
|
|
||||||
|
If all goes well, you would be able to publish to one relay and watch from the other.
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Change directory to the root of the project
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Run the API server on port 4442 by default
|
||||||
|
HOST="${HOST:-[::]}"
|
||||||
|
PORT="${PORT:-4442}"
|
||||||
|
LISTEN="${LISTEN:-$HOST:$PORT}"
|
||||||
|
|
||||||
|
# Default to info log level
|
||||||
|
export RUST_LOG="${RUST_LOG:-info}"
|
||||||
|
|
||||||
|
# Check for Podman/Docker and set runtime accordingly
|
||||||
|
if command -v podman &> /dev/null; then
|
||||||
|
RUNTIME=podman
|
||||||
|
elif command -v docker &> /dev/null; then
|
||||||
|
RUNTIME=docker
|
||||||
|
else
|
||||||
|
echo "Neither podman or docker found in PATH. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REDIS_PORT=${REDIS_PORT:-6400} # The default is 6379, but we'll use 6400 to avoid conflicts
|
||||||
|
|
||||||
|
# Cleanup function to stop Redis when script exits
|
||||||
|
cleanup() {
|
||||||
|
$RUNTIME rm -f moq-redis || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop the redis instance if it's still running
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
# Run a Redis instance
|
||||||
|
REDIS_CONTAINER=$($RUNTIME run --rm --name moq-redis -d -p "$REDIS_PORT:6379" redis:latest)
|
||||||
|
|
||||||
|
# Cleanup function to stop Redis when script exits
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Default to a sqlite database in memory
|
||||||
|
DATABASE="${DATABASE-sqlite::memory:}"
|
||||||
|
|
||||||
|
# Run the relay and forward any arguments
|
||||||
|
cargo run --bin moq-api -- --listen "$LISTEN" --redis "redis://localhost:$REDIS_PORT" "$@"
|
15
dev/pub
15
dev/pub
|
@ -4,22 +4,29 @@ set -euo pipefail
|
||||||
# Change directory to the root of the project
|
# Change directory to the root of the project
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
export RUST_LOG="${RUST_LOG:-info}"
|
||||||
|
|
||||||
# Connect to localhost by default.
|
# Connect to localhost by default.
|
||||||
HOST="${HOST:-localhost:4443}"
|
HOST="${HOST:-localhost}"
|
||||||
|
PORT="${PORT:-4443}"
|
||||||
|
ADDR="${ADDR:-$HOST:$PORT}"
|
||||||
|
|
||||||
# Generate a random 16 character name by default.
|
# Generate a random 16 character name by default.
|
||||||
NAME="${NAME:-$(head /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 16)}"
|
NAME="${NAME:-$(head /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 16)}"
|
||||||
|
|
||||||
# Combine the host and name into a URI.
|
# Combine the host and name into a URL.
|
||||||
URI="${URI:-"moq://$HOST/$NAME"}"
|
URL="${URL:-"https://$ADDR/$NAME"}"
|
||||||
|
|
||||||
# Default to a source video
|
# Default to a source video
|
||||||
MEDIA="${MEDIA:-dev/source.mp4}"
|
MEDIA="${MEDIA:-dev/source.mp4}"
|
||||||
|
|
||||||
|
# Print out the watch URL
|
||||||
|
echo "Watch URL: https://quic.video/watch/$NAME?server=$ADDR"
|
||||||
|
|
||||||
# Run ffmpeg and pipe the output to moq-pub
|
# Run ffmpeg and pipe the output to moq-pub
|
||||||
ffmpeg -hide_banner -v quiet \
|
ffmpeg -hide_banner -v quiet \
|
||||||
-stream_loop -1 -re \
|
-stream_loop -1 -re \
|
||||||
-i "$MEDIA" \
|
-i "$MEDIA" \
|
||||||
-an \
|
-an \
|
||||||
-f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - \
|
-f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - \
|
||||||
| RUST_LOG=info cargo run --bin moq-pub -- "$URI" "$@"
|
| RUST_LOG=info cargo run --bin moq-pub -- "$URL" "$@"
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Change directory to the root of the project
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Connect to the 2nd relay by default.
|
||||||
|
export PORT="${PORT:-4444}"
|
||||||
|
|
||||||
|
./dev/pub
|
26
dev/relay
26
dev/relay
|
@ -4,10 +4,34 @@ set -euo pipefail
|
||||||
# Change directory to the root of the project
|
# Change directory to the root of the project
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Use info logging by default
|
||||||
|
export RUST_LOG="${RUST_LOG:-info}"
|
||||||
|
|
||||||
# Default to a self-signed certificate
|
# Default to a self-signed certificate
|
||||||
# TODO automatically generate if it doesn't exist.
|
# TODO automatically generate if it doesn't exist.
|
||||||
CERT="${CERT:-dev/localhost.crt}"
|
CERT="${CERT:-dev/localhost.crt}"
|
||||||
KEY="${KEY:-dev/localhost.key}"
|
KEY="${KEY:-dev/localhost.key}"
|
||||||
|
|
||||||
|
# Default to listening on localhost:4443
|
||||||
|
HOST="${HOST:-[::]}"
|
||||||
|
PORT="${PORT:-4443}"
|
||||||
|
LISTEN="${LISTEN:-$HOST:$PORT}"
|
||||||
|
|
||||||
|
# A list of optional args
|
||||||
|
ARGS=""
|
||||||
|
|
||||||
|
# Connect to the given URL to get origins.
|
||||||
|
# TODO default to a public instance?
|
||||||
|
if [ -n "$API" ]; then
|
||||||
|
ARGS="$ARGS --api $API"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Provide our node URL when registering origins.
|
||||||
|
if [ -n "$NODE" ]; then
|
||||||
|
ARGS="$ARGS --node $NODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Publish URL: https://quic.video/publish/?server=localhost:${PORT}"
|
||||||
|
|
||||||
# Run the relay and forward any arguments
|
# Run the relay and forward any arguments
|
||||||
RUST_LOG=info cargo run --bin moq-relay -- --cert "$CERT" --key "$KEY" --fingerprint "$@"
|
cargo run --bin moq-relay -- --listen "$LISTEN" --cert "$CERT" --key "$KEY" --fingerprint $ARGS -- "$@"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Change directory to the root of the project
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Run an instance that advertises itself to the origin API.
|
||||||
|
export PORT="${PORT:-4443}"
|
||||||
|
export API="${API:-http://localhost:4442}" # TODO support HTTPS
|
||||||
|
export NODE="${NODE:-https://localhost:$PORT}"
|
||||||
|
|
||||||
|
./dev/relay
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Change directory to the root of the project
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Run an instance that advertises itself to the origin API.
|
||||||
|
export PORT="${PORT:-4444}"
|
||||||
|
export API="${API:-http://localhost:4442}" # TODO support HTTPS
|
||||||
|
export NODE="${NODE:-https://localhost:$PORT}"
|
||||||
|
|
||||||
|
./dev/relay
|
|
@ -0,0 +1,44 @@
|
||||||
|
[package]
|
||||||
|
name = "moq-api"
|
||||||
|
description = "Media over QUIC"
|
||||||
|
authors = ["Luke Curley"]
|
||||||
|
repository = "https://github.com/kixelated/moq-rs"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
keywords = ["quic", "http3", "webtransport", "media", "live"]
|
||||||
|
categories = ["multimedia", "network-programming", "web-programming"]
|
||||||
|
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# HTTP server
|
||||||
|
axum = "0.6"
|
||||||
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tower = "0.4"
|
||||||
|
|
||||||
|
# HTTP client
|
||||||
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||||
|
|
||||||
|
# JSON encoding
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
|
# Database
|
||||||
|
redis = { version = "0.23", features = [
|
||||||
|
"tokio-rustls-comp",
|
||||||
|
"connection-manager",
|
||||||
|
] }
|
||||||
|
url = { version = "2", features = ["serde"] }
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.9"
|
||||||
|
thiserror = "1"
|
|
@ -0,0 +1,4 @@
|
||||||
|
# moq-api
|
||||||
|
|
||||||
|
A thin HTTP API that wraps Redis.
|
||||||
|
Basically I didn't want the relays connecting to Redis directly.
|
|
@ -0,0 +1,47 @@
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{ApiError, Origin};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
// The address of the moq-api server
|
||||||
|
url: Url,
|
||||||
|
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(url: Url) -> Self {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
Self { url, client }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_origin(&self, id: &str) -> Result<Option<Origin>, ApiError> {
|
||||||
|
let url = self.url.join("origin/")?.join(id)?;
|
||||||
|
let resp = self.client.get(url).send().await?;
|
||||||
|
if resp.status() == reqwest::StatusCode::NOT_FOUND {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let origin: Origin = resp.json().await?;
|
||||||
|
Ok(Some(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_origin(&mut self, id: &str, origin: Origin) -> Result<(), ApiError> {
|
||||||
|
let url = self.url.join("origin/")?.join(id)?;
|
||||||
|
|
||||||
|
let resp = self.client.post(url).json(&origin).send().await?;
|
||||||
|
resp.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_origin(&mut self, id: &str) -> Result<(), ApiError> {
|
||||||
|
let url = self.url.join("origin/")?.join(id)?;
|
||||||
|
|
||||||
|
let resp = self.client.delete(url).send().await?;
|
||||||
|
resp.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ApiError {
|
||||||
|
#[error("redis error: {0}")]
|
||||||
|
Redis(#[from] redis::RedisError),
|
||||||
|
|
||||||
|
#[error("reqwest error: {0}")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("hyper error: {0}")]
|
||||||
|
Hyper(#[from] hyper::Error),
|
||||||
|
|
||||||
|
#[error("url error: {0}")]
|
||||||
|
Url(#[from] url::ParseError),
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod client;
|
||||||
|
mod error;
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
pub use client::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use model::*;
|
|
@ -0,0 +1,14 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
use moq_api::ApiError;
|
||||||
|
use server::{Server, ServerConfig};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), ApiError> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let config = ServerConfig::parse();
|
||||||
|
let server = Server::new(config);
|
||||||
|
server.run().await
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Origin {
|
||||||
|
pub url: Url,
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
use std::net;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::get,
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use redis::{aio::ConnectionManager, AsyncCommands};
|
||||||
|
|
||||||
|
use moq_api::{ApiError, Origin};
|
||||||
|
|
||||||
|
/// Runs a HTTP API to create/get origins for broadcasts.
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
/// Listen for HTTP requests on the given address
|
||||||
|
#[arg(long)]
|
||||||
|
pub listen: net::SocketAddr,
|
||||||
|
|
||||||
|
/// Connect to the given redis instance
|
||||||
|
#[arg(long)]
|
||||||
|
pub redis: url::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
config: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn new(config: ServerConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self) -> Result<(), ApiError> {
|
||||||
|
log::info!("connecting to redis: url={}", self.config.redis);
|
||||||
|
|
||||||
|
// Create the redis client.
|
||||||
|
let redis = redis::Client::open(self.config.redis)?;
|
||||||
|
let redis = redis
|
||||||
|
.get_tokio_connection_manager() // TODO get_tokio_connection_manager_with_backoff?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/origin/:id", get(get_origin).post(set_origin).delete(delete_origin))
|
||||||
|
.with_state(redis);
|
||||||
|
|
||||||
|
log::info!("serving requests: bind={}", self.config.listen);
|
||||||
|
|
||||||
|
axum::Server::bind(&self.config.listen)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_origin(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(mut redis): State<ConnectionManager>,
|
||||||
|
) -> Result<Json<Origin>, AppError> {
|
||||||
|
let key = origin_key(&id);
|
||||||
|
|
||||||
|
log::debug!("get_origin: id={}", id);
|
||||||
|
|
||||||
|
let payload: String = match redis.get(&key).await? {
|
||||||
|
Some(payload) => payload,
|
||||||
|
None => return Err(AppError::NotFound),
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin: Origin = serde_json::from_str(&payload)?;
|
||||||
|
|
||||||
|
Ok(Json(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_origin(
|
||||||
|
State(mut redis): State<ConnectionManager>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(origin): Json<Origin>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
// TODO validate origin
|
||||||
|
|
||||||
|
let key = origin_key(&id);
|
||||||
|
|
||||||
|
// Convert the input back to JSON after validating it add adding any fields (TODO)
|
||||||
|
let payload = serde_json::to_string(&origin)?;
|
||||||
|
|
||||||
|
let res: Option<String> = redis::cmd("SET")
|
||||||
|
.arg(key)
|
||||||
|
.arg(payload)
|
||||||
|
.arg("NX")
|
||||||
|
.arg("EX")
|
||||||
|
.arg(60 * 60 * 24 * 2) // Set the key to expire in 2 days; just in case we forget to remove it.
|
||||||
|
.query_async(&mut redis)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.is_none() {
|
||||||
|
return Err(AppError::Duplicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_origin(Path(id): Path<String>, State(mut redis): State<ConnectionManager>) -> Result<(), AppError> {
|
||||||
|
let key = origin_key(&id);
|
||||||
|
match redis.del(key).await? {
|
||||||
|
0 => Err(AppError::NotFound),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn origin_key(id: &str) -> String {
|
||||||
|
format!("origin.{}", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum AppError {
|
||||||
|
#[error("redis error")]
|
||||||
|
Redis(#[from] redis::RedisError),
|
||||||
|
|
||||||
|
#[error("json error")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("duplicate ID")]
|
||||||
|
Duplicate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell axum how to convert `AppError` into a response.
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
match self {
|
||||||
|
AppError::Redis(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("redis error: {}", e)).into_response(),
|
||||||
|
AppError::Json(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("json error: {}", e)).into_response(),
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
AppError::Duplicate => StatusCode::CONFLICT.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,29 +18,29 @@ moq-transport = { path = "../moq-transport" }
|
||||||
|
|
||||||
# QUIC
|
# QUIC
|
||||||
quinn = "0.10"
|
quinn = "0.10"
|
||||||
webtransport-quinn = "0.5"
|
webtransport-quinn = "0.6"
|
||||||
webtransport-generic = "0.5"
|
#webtransport-quinn = { path = "../../webtransport-rs/webtransport-quinn" }
|
||||||
http = "0.2.9"
|
url = "2"
|
||||||
|
|
||||||
# Crypto
|
# Crypto
|
||||||
ring = "0.16.20"
|
ring = "0.16"
|
||||||
rustls = "0.21.2"
|
rustls = "0.21"
|
||||||
rustls-pemfile = "1.0.2"
|
rustls-pemfile = "1"
|
||||||
|
|
||||||
# Async stuff
|
# Async stuff
|
||||||
tokio = { version = "1.27", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
# CLI, logging, error handling
|
# CLI, logging, error handling
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
env_logger = "0.9.3"
|
env_logger = "0.9"
|
||||||
mp4 = "0.13.0"
|
mp4 = "0.13"
|
||||||
rustls-native-certs = "0.6.3"
|
rustls-native-certs = "0.6"
|
||||||
anyhow = { version = "1.0.70", features = ["backtrace"] }
|
anyhow = { version = "1", features = ["backtrace"] }
|
||||||
serde_json = "1.0.105"
|
serde_json = "1"
|
||||||
rfc6381-codec = "0.1.0"
|
rfc6381-codec = "0.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
http = "0.2.9"
|
clap = { version = "4", features = ["derive"] }
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap_mangen = "0.2"
|
||||||
clap_mangen = "0.2.12"
|
url = "2"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::net;
|
use std::net;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -17,18 +18,18 @@ pub struct Config {
|
||||||
#[arg(long, default_value = "1500000")]
|
#[arg(long, default_value = "1500000")]
|
||||||
pub bitrate: u32,
|
pub bitrate: u32,
|
||||||
|
|
||||||
/// Connect to the given URI starting with moq://
|
/// Connect to the given URL starting with https://
|
||||||
#[arg(value_parser = moq_uri)]
|
#[arg(value_parser = moq_url)]
|
||||||
pub uri: http::Uri,
|
pub url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moq_uri(s: &str) -> Result<http::Uri, String> {
|
fn moq_url(s: &str) -> Result<Url, String> {
|
||||||
let uri = http::Uri::try_from(s).map_err(|e| e.to_string())?;
|
let url = Url::try_from(s).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Make sure the scheme is moq
|
// Make sure the scheme is moq
|
||||||
if uri.scheme_str() != Some("moq") {
|
if url.scheme() != "https" {
|
||||||
return Err("uri scheme must be moq".to_string());
|
return Err("url scheme must be https:// for WebTransport".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(uri)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use cli::*;
|
||||||
mod media;
|
mod media;
|
||||||
use media::*;
|
use media::*;
|
||||||
|
|
||||||
use moq_transport::model::broadcast;
|
use moq_transport::cache::broadcast;
|
||||||
|
|
||||||
// TODO: clap complete
|
// TODO: clap complete
|
||||||
|
|
||||||
|
@ -39,14 +39,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let mut endpoint = quinn::Endpoint::client(config.bind)?;
|
let mut endpoint = quinn::Endpoint::client(config.bind)?;
|
||||||
endpoint.set_default_client_config(quinn_client_config);
|
endpoint.set_default_client_config(quinn_client_config);
|
||||||
|
|
||||||
log::info!("connecting to {}", config.uri);
|
log::info!("connecting to relay: url={}", config.url);
|
||||||
|
|
||||||
// Change the uri scheme to "https" for WebTransport
|
let session = webtransport_quinn::connect(&endpoint, &config.url)
|
||||||
let mut parts = config.uri.into_parts();
|
|
||||||
parts.scheme = Some(http::uri::Scheme::HTTPS);
|
|
||||||
let uri = http::Uri::from_parts(parts)?;
|
|
||||||
|
|
||||||
let session = webtransport_quinn::connect(&endpoint, &uri)
|
|
||||||
.await
|
.await
|
||||||
.context("failed to create WebTransport session")?;
|
.context("failed to create WebTransport session")?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::cli::Config;
|
use crate::cli::Config;
|
||||||
use anyhow::{self, Context};
|
use anyhow::{self, Context};
|
||||||
use moq_transport::model::{broadcast, segment, track};
|
use moq_transport::cache::{broadcast, segment, track};
|
||||||
use moq_transport::VarInt;
|
use moq_transport::VarInt;
|
||||||
use mp4::{self, ReadBox};
|
use mp4::{self, ReadBox};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
|
@ -13,28 +13,35 @@ categories = ["multimedia", "network-programming", "web-programming"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
moq-transport = { path = "../moq-transport" }
|
moq-transport = { path = "../moq-transport" }
|
||||||
|
moq-api = { path = "../moq-api" }
|
||||||
|
|
||||||
# QUIC
|
# QUIC
|
||||||
quinn = "0.10"
|
quinn = "0.10"
|
||||||
webtransport-generic = "0.5"
|
webtransport-quinn = "0.6"
|
||||||
webtransport-quinn = "0.5"
|
#webtransport-quinn = { path = "../../webtransport-rs/webtransport-quinn" }
|
||||||
|
url = "2"
|
||||||
|
|
||||||
# Crypto
|
# Crypto
|
||||||
ring = "0.16.20"
|
ring = "0.16"
|
||||||
rustls = "0.21.2"
|
rustls = "0.21"
|
||||||
rustls-pemfile = "1.0.2"
|
rustls-pemfile = "1"
|
||||||
|
rustls-native-certs = "0.6"
|
||||||
|
|
||||||
# Async stuff
|
# Async stuff
|
||||||
tokio = { version = "1.27", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
# Web server to serve the fingerprint
|
# Web server to serve the fingerprint
|
||||||
warp = { version = "0.3.3", features = ["tls"] }
|
warp = { version = "0.3", features = ["tls"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
anyhow = { version = "1", features = ["backtrace"] }
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
env_logger = "0.9.3"
|
env_logger = "0.9"
|
||||||
anyhow = "1.0.70"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3.0"
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{net, path};
|
use std::{net, path};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ use clap::Parser;
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Listen on this address
|
/// Listen on this address
|
||||||
#[arg(long, default_value = "[::]:4443")]
|
#[arg(long, default_value = "[::]:4443")]
|
||||||
pub bind: net::SocketAddr,
|
pub listen: net::SocketAddr,
|
||||||
|
|
||||||
/// Use the certificate file at this path
|
/// Use the certificate file at this path
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
@ -20,4 +21,15 @@ pub struct Config {
|
||||||
/// Listen on HTTPS and serve /fingerprint, for self-signed certificates
|
/// Listen on HTTPS and serve /fingerprint, for self-signed certificates
|
||||||
#[arg(long, action)]
|
#[arg(long, action)]
|
||||||
pub fingerprint: bool,
|
pub fingerprint: bool,
|
||||||
|
|
||||||
|
/// Optional: Use the moq-api via HTTP to store origin information.
|
||||||
|
#[arg(long)]
|
||||||
|
pub api: Option<Url>,
|
||||||
|
|
||||||
|
/// Our internal address which we advertise to other origins.
|
||||||
|
/// We use QUIC, so the certificate must be valid for this address.
|
||||||
|
/// This needs to be prefixed with https:// to use WebTransport
|
||||||
|
/// This is only used when --api is set.
|
||||||
|
#[arg(long)]
|
||||||
|
pub node: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RelayError {
|
||||||
|
#[error("transport error: {0}")]
|
||||||
|
Transport(#[from] moq_transport::session::SessionError),
|
||||||
|
|
||||||
|
#[error("cache error: {0}")]
|
||||||
|
Cache(#[from] moq_transport::cache::CacheError),
|
||||||
|
|
||||||
|
#[error("api error: {0}")]
|
||||||
|
MoqApi(#[from] moq_api::ApiError),
|
||||||
|
|
||||||
|
#[error("url error: {0}")]
|
||||||
|
Url(#[from] url::ParseError),
|
||||||
|
|
||||||
|
#[error("webtransport client error: {0}")]
|
||||||
|
WebTransportClient(#[from] webtransport_quinn::ClientError),
|
||||||
|
|
||||||
|
#[error("webtransport server error: {0}")]
|
||||||
|
WebTransportServer(#[from] webtransport_quinn::ServerError),
|
||||||
|
|
||||||
|
#[error("missing node")]
|
||||||
|
MissingNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl moq_transport::MoqError for RelayError {
|
||||||
|
fn code(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::Transport(err) => err.code(),
|
||||||
|
Self::Cache(err) => err.code(),
|
||||||
|
Self::MoqApi(_err) => 504,
|
||||||
|
Self::Url(_) => 500,
|
||||||
|
Self::MissingNode => 500,
|
||||||
|
Self::WebTransportClient(_) => 504,
|
||||||
|
Self::WebTransportServer(_) => 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reason(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Transport(err) => err.reason(),
|
||||||
|
Self::Cache(err) => err.reason(),
|
||||||
|
Self::MoqApi(_err) => "api error",
|
||||||
|
Self::Url(_) => "url error",
|
||||||
|
Self::MissingNode => "missing node",
|
||||||
|
Self::WebTransportServer(_) => "server error",
|
||||||
|
Self::WebTransportClient(_) => "upstream error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,14 @@ use ring::digest::{digest, SHA256};
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod error;
|
||||||
|
mod origin;
|
||||||
mod server;
|
mod server;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use origin::*;
|
||||||
pub use server::*;
|
pub use server::*;
|
||||||
pub use session::*;
|
pub use session::*;
|
||||||
|
|
||||||
|
@ -18,15 +22,17 @@ async fn main() -> anyhow::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// Disable tracing so we don't get a bunch of Quinn spam.
|
// Disable tracing so we don't get a bunch of Quinn spam.
|
||||||
|
/*
|
||||||
let tracer = tracing_subscriber::FmtSubscriber::builder()
|
let tracer = tracing_subscriber::FmtSubscriber::builder()
|
||||||
.with_max_level(tracing::Level::WARN)
|
.with_max_level(tracing::Level::WARN)
|
||||||
.finish();
|
.finish();
|
||||||
tracing::subscriber::set_global_default(tracer).unwrap();
|
tracing::subscriber::set_global_default(tracer).unwrap();
|
||||||
|
*/
|
||||||
|
|
||||||
let config = Config::parse();
|
let config = Config::parse();
|
||||||
|
|
||||||
// Create a server to actually serve the media
|
// Create a server to actually serve the media
|
||||||
let server = Server::new(config.clone()).context("failed to create server")?;
|
let server = Server::new(config.clone()).await.context("failed to create server")?;
|
||||||
|
|
||||||
// Run all of the above
|
// Run all of the above
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
@ -63,7 +69,7 @@ async fn serve_http(config: Config) -> anyhow::Result<()> {
|
||||||
.tls()
|
.tls()
|
||||||
.cert_path(config.cert)
|
.cert_path(config.cert)
|
||||||
.key_path(config.key)
|
.key_path(config.key)
|
||||||
.run(config.bind)
|
.run(config.listen)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
use std::{
|
||||||
|
collections::{hash_map, HashMap},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use moq_transport::cache::{broadcast, CacheError};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::RelayError;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Origin {
|
||||||
|
// An API client used to get/set broadcasts.
|
||||||
|
// If None then we never use a remote origin.
|
||||||
|
api: Option<moq_api::Client>,
|
||||||
|
|
||||||
|
// The internal address of our node.
|
||||||
|
// If None then we can never advertise ourselves as an origin.
|
||||||
|
node: Option<Url>,
|
||||||
|
|
||||||
|
// A map of active broadcasts.
|
||||||
|
lookup: Arc<Mutex<HashMap<String, broadcast::Subscriber>>>,
|
||||||
|
|
||||||
|
// A QUIC endpoint we'll use to fetch from other origins.
|
||||||
|
quic: quinn::Endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Origin {
|
||||||
|
pub fn new(api: Option<moq_api::Client>, node: Option<Url>, quic: quinn::Endpoint) -> Self {
|
||||||
|
Self {
|
||||||
|
api,
|
||||||
|
node,
|
||||||
|
lookup: Default::default(),
|
||||||
|
quic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_broadcast(&mut self, id: &str) -> Result<broadcast::Publisher, RelayError> {
|
||||||
|
let (publisher, subscriber) = broadcast::new();
|
||||||
|
|
||||||
|
// Check if a broadcast already exists by that id.
|
||||||
|
match self.lookup.lock().unwrap().entry(id.to_string()) {
|
||||||
|
hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate.into()),
|
||||||
|
hash_map::Entry::Vacant(v) => v.insert(subscriber),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref mut api) = self.api {
|
||||||
|
// Make a URL for the broadcast.
|
||||||
|
let url = self.node.as_ref().ok_or(RelayError::MissingNode)?.clone().join(id)?;
|
||||||
|
|
||||||
|
log::info!("announcing origin: id={} url={}", id, url);
|
||||||
|
|
||||||
|
let entry = moq_api::Origin { url };
|
||||||
|
|
||||||
|
if let Err(err) = api.set_origin(id, entry).await {
|
||||||
|
self.lookup.lock().unwrap().remove(id);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(publisher)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_broadcast(&self, id: &str) -> broadcast::Subscriber {
|
||||||
|
let mut lookup = self.lookup.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(broadcast) = lookup.get(id) {
|
||||||
|
if broadcast.closed().is_none() {
|
||||||
|
return broadcast.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (publisher, subscriber) = broadcast::new();
|
||||||
|
lookup.insert(id.to_string(), subscriber.clone());
|
||||||
|
|
||||||
|
let mut this = self.clone();
|
||||||
|
let id = id.to_string();
|
||||||
|
|
||||||
|
// Rather than fetching from the API and connecting via QUIC inline, we'll spawn a task to do it.
|
||||||
|
// This way we could stop polling this session and it won't impact other session.
|
||||||
|
// It also means we'll only connect the API and QUIC once if N subscribers suddenly show up.
|
||||||
|
// However, the downside is that we don't return an error immediately.
|
||||||
|
// If that's important, it can be done but it gets a bit racey.
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match this.fetch_broadcast(&id).await {
|
||||||
|
Ok(session) => {
|
||||||
|
if let Err(err) = this.run_broadcast(session, publisher).await {
|
||||||
|
log::warn!("failed to run broadcast: id={} err={:#?}", id, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to fetch broadcast: id={} err={:#?}", id, err);
|
||||||
|
publisher.close(CacheError::NotFound).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_broadcast(&mut self, id: &str) -> Result<webtransport_quinn::Session, RelayError> {
|
||||||
|
// Fetch the origin from the API.
|
||||||
|
let api = match self.api {
|
||||||
|
Some(ref mut api) => api,
|
||||||
|
|
||||||
|
// We return NotFound here instead of earlier just to simulate an API fetch.
|
||||||
|
None => return Err(CacheError::NotFound.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("fetching origin: id={}", id);
|
||||||
|
|
||||||
|
let origin = api.get_origin(id).await?.ok_or(CacheError::NotFound)?;
|
||||||
|
|
||||||
|
log::info!("connecting to origin: url={}", origin.url);
|
||||||
|
|
||||||
|
// Establish the webtransport session.
|
||||||
|
let session = webtransport_quinn::connect(&self.quic, &origin.url).await?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_broadcast(
|
||||||
|
&mut self,
|
||||||
|
session: webtransport_quinn::Session,
|
||||||
|
broadcast: broadcast::Publisher,
|
||||||
|
) -> Result<(), RelayError> {
|
||||||
|
let session = moq_transport::session::Client::subscriber(session, broadcast).await?;
|
||||||
|
|
||||||
|
session.run().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_broadcast(&mut self, id: &str) -> Result<(), RelayError> {
|
||||||
|
self.lookup.lock().unwrap().remove(id).ok_or(CacheError::NotFound)?;
|
||||||
|
|
||||||
|
if let Some(ref mut api) = self.api {
|
||||||
|
log::info!("deleting origin: id={}", id);
|
||||||
|
api.delete_origin(id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,40 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
fs,
|
fs,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
sync::{Arc, Mutex},
|
sync::Arc,
|
||||||
time,
|
time,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use moq_transport::model::broadcast;
|
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::{Config, Session};
|
use crate::{Config, Origin, Session};
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
server: quinn::Endpoint,
|
quic: quinn::Endpoint,
|
||||||
|
|
||||||
// The active connections.
|
// The active connections.
|
||||||
conns: JoinSet<anyhow::Result<()>>,
|
conns: JoinSet<anyhow::Result<()>>,
|
||||||
|
|
||||||
// The map of active broadcasts by path.
|
// The map of active broadcasts by path.
|
||||||
broadcasts: Arc<Mutex<HashMap<String, broadcast::Subscriber>>>,
|
origin: Origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
// Create a new server
|
// Create a new server
|
||||||
pub fn new(config: Config) -> anyhow::Result<Self> {
|
pub async fn new(config: Config) -> anyhow::Result<Self> {
|
||||||
// Read the PEM certificate chain
|
// Read the PEM certificate chain
|
||||||
let certs = fs::File::open(config.cert).context("failed to open cert file")?;
|
let certs = fs::File::open(config.cert).context("failed to open cert file")?;
|
||||||
let mut certs = io::BufReader::new(certs);
|
let mut certs = io::BufReader::new(certs);
|
||||||
let certs = rustls_pemfile::certs(&mut certs)?
|
|
||||||
|
let certs: Vec<rustls::Certificate> = rustls_pemfile::certs(&mut certs)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(rustls::Certificate)
|
.map(rustls::Certificate)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
anyhow::ensure!(!certs.is_empty(), "could not find certificate");
|
||||||
|
|
||||||
// Read the PEM private key
|
// Read the PEM private key
|
||||||
let mut keys = fs::File::open(config.key).context("failed to open key file")?;
|
let mut keys = fs::File::open(config.key).context("failed to open key file")?;
|
||||||
|
|
||||||
|
@ -56,46 +57,84 @@ impl Server {
|
||||||
|
|
||||||
let key = rustls::PrivateKey(keys.remove(0));
|
let key = rustls::PrivateKey(keys.remove(0));
|
||||||
|
|
||||||
let mut tls_config = rustls::ServerConfig::builder()
|
// Set up a QUIC endpoint that can act as both a client and server.
|
||||||
|
|
||||||
|
// Create a list of acceptable root certificates.
|
||||||
|
let mut client_roots = rustls::RootCertStore::empty();
|
||||||
|
|
||||||
|
// For local development, we'll accept our own certificate.
|
||||||
|
for cert in &certs {
|
||||||
|
client_roots.add(cert).context("failed to add our cert to roots")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the platform's native root certificates.
|
||||||
|
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
|
||||||
|
client_roots.add(&rustls::Certificate(cert.0)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client_config = rustls::ClientConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_root_certificates(client_roots)
|
||||||
|
.with_no_client_auth();
|
||||||
|
|
||||||
|
let mut server_config = rustls::ServerConfig::builder()
|
||||||
.with_safe_default_cipher_suites()
|
.with_safe_default_cipher_suites()
|
||||||
.with_safe_default_kx_groups()
|
.with_safe_default_kx_groups()
|
||||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||||
.unwrap()
|
.context("failed to create server config")?
|
||||||
.with_no_client_auth()
|
.with_no_client_auth()
|
||||||
.with_single_cert(certs, key)?;
|
.with_single_cert(certs, key)?;
|
||||||
|
|
||||||
tls_config.max_early_data_size = u32::MAX;
|
server_config.max_early_data_size = u32::MAX;
|
||||||
tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()];
|
client_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()];
|
||||||
|
server_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()];
|
||||||
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(tls_config));
|
|
||||||
|
|
||||||
// Enable BBR congestion control
|
// Enable BBR congestion control
|
||||||
// TODO validate the implementation
|
// TODO validate the implementation
|
||||||
let mut transport_config = quinn::TransportConfig::default();
|
let mut transport_config = quinn::TransportConfig::default();
|
||||||
transport_config.keep_alive_interval(Some(time::Duration::from_secs(2)));
|
transport_config.max_idle_timeout(Some(time::Duration::from_secs(10).try_into().unwrap()));
|
||||||
|
transport_config.keep_alive_interval(Some(time::Duration::from_secs(4))); // TODO make this smarter
|
||||||
transport_config.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default()));
|
transport_config.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default()));
|
||||||
|
let transport_config = Arc::new(transport_config);
|
||||||
|
|
||||||
server_config.transport = Arc::new(transport_config);
|
let mut client_config = quinn::ClientConfig::new(Arc::new(client_config));
|
||||||
let server = quinn::Endpoint::server(server_config, config.bind)?;
|
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(server_config));
|
||||||
|
server_config.transport_config(transport_config.clone());
|
||||||
|
client_config.transport_config(transport_config);
|
||||||
|
|
||||||
let broadcasts = Default::default();
|
// There's a bit more boilerplate to make a generic endpoint.
|
||||||
|
let runtime = quinn::default_runtime().context("no async runtime")?;
|
||||||
|
let endpoint_config = quinn::EndpointConfig::default();
|
||||||
|
let socket = std::net::UdpSocket::bind(config.listen).context("failed to bind UDP socket")?;
|
||||||
|
|
||||||
|
// Create the generic QUIC endpoint.
|
||||||
|
let mut quic = quinn::Endpoint::new(endpoint_config, Some(server_config), socket, runtime)
|
||||||
|
.context("failed to create QUIC endpoint")?;
|
||||||
|
quic.set_default_client_config(client_config);
|
||||||
|
|
||||||
|
let api = config.api.map(|url| {
|
||||||
|
log::info!("using moq-api: url={}", url);
|
||||||
|
moq_api::Client::new(url)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref node) = config.node {
|
||||||
|
log::info!("advertising origin: url={}", node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let origin = Origin::new(api, config.node, quic.clone());
|
||||||
let conns = JoinSet::new();
|
let conns = JoinSet::new();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { quic, origin, conns })
|
||||||
server,
|
|
||||||
broadcasts,
|
|
||||||
conns,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self) -> anyhow::Result<()> {
|
pub async fn run(mut self) -> anyhow::Result<()> {
|
||||||
log::info!("listening on {}", self.server.local_addr()?);
|
log::info!("listening on {}", self.quic.local_addr()?);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = self.server.accept() => {
|
res = self.quic.accept() => {
|
||||||
let conn = res.context("failed to accept QUIC connection")?;
|
let conn = res.context("failed to accept QUIC connection")?;
|
||||||
let mut session = Session::new(self.broadcasts.clone());
|
let mut session = Session::new(self.origin.clone());
|
||||||
self.conns.spawn(async move { session.run(conn).await });
|
self.conns.spawn(async move { session.run(conn).await });
|
||||||
},
|
},
|
||||||
res = self.conns.join_next(), if !self.conns.is_empty() => {
|
res = self.conns.join_next(), if !self.conns.is_empty() => {
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
use std::{
|
|
||||||
collections::{hash_map, HashMap},
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use moq_transport::{model::broadcast, session::Request, setup::Role};
|
use moq_transport::{cache::broadcast, session::Request, setup::Role, MoqError};
|
||||||
|
|
||||||
|
use crate::Origin;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
broadcasts: Arc<Mutex<HashMap<String, broadcast::Subscriber>>>,
|
origin: Origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(broadcasts: Arc<Mutex<HashMap<String, broadcast::Subscriber>>>) -> Self {
|
pub fn new(origin: Origin) -> Self {
|
||||||
Self { broadcasts }
|
Self { origin }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self, conn: quinn::Connecting) -> anyhow::Result<()> {
|
pub async fn run(&mut self, conn: quinn::Connecting) -> anyhow::Result<()> {
|
||||||
|
@ -35,7 +32,8 @@ impl Session {
|
||||||
.await
|
.await
|
||||||
.context("failed to receive WebTransport request")?;
|
.context("failed to receive WebTransport request")?;
|
||||||
|
|
||||||
let path = request.uri().path().to_string();
|
// Strip any leading and trailing slashes to get the broadcast name.
|
||||||
|
let path = request.url().path().trim_matches('/').to_string();
|
||||||
|
|
||||||
log::debug!("received WebTransport CONNECT: id={} path={}", id, path);
|
log::debug!("received WebTransport CONNECT: id={} path={}", id, path);
|
||||||
|
|
||||||
|
@ -45,8 +43,6 @@ impl Session {
|
||||||
.await
|
.await
|
||||||
.context("failed to respond to WebTransport request")?;
|
.context("failed to respond to WebTransport request")?;
|
||||||
|
|
||||||
log::debug!("accepted WebTransport CONNECT: id={} path={}", id, path);
|
|
||||||
|
|
||||||
// Perform the MoQ handshake.
|
// Perform the MoQ handshake.
|
||||||
let request = moq_transport::session::Server::accept(session)
|
let request = moq_transport::session::Server::accept(session)
|
||||||
.await
|
.await
|
||||||
|
@ -59,7 +55,10 @@ impl Session {
|
||||||
match role {
|
match role {
|
||||||
Role::Publisher => self.serve_publisher(id, request, &path).await,
|
Role::Publisher => self.serve_publisher(id, request, &path).await,
|
||||||
Role::Subscriber => self.serve_subscriber(id, request, &path).await,
|
Role::Subscriber => self.serve_subscriber(id, request, &path).await,
|
||||||
Role::Both => request.reject(300),
|
Role::Both => {
|
||||||
|
log::warn!("role both not supported: id={}", id);
|
||||||
|
request.reject(300);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("closing connection: id={}", id);
|
log::debug!("closing connection: id={}", id);
|
||||||
|
@ -70,18 +69,20 @@ impl Session {
|
||||||
async fn serve_publisher(&mut self, id: usize, request: Request, path: &str) {
|
async fn serve_publisher(&mut self, id: usize, request: Request, path: &str) {
|
||||||
log::info!("serving publisher: id={}, path={}", id, path);
|
log::info!("serving publisher: id={}, path={}", id, path);
|
||||||
|
|
||||||
let (publisher, subscriber) = broadcast::new();
|
let broadcast = match self.origin.create_broadcast(path).await {
|
||||||
|
Ok(broadcast) => broadcast,
|
||||||
match self.broadcasts.lock().unwrap().entry(path.to_string()) {
|
Err(err) => {
|
||||||
hash_map::Entry::Occupied(_) => return request.reject(409),
|
log::warn!("error accepting publisher: id={} path={} err={:#?}", id, path, err);
|
||||||
hash_map::Entry::Vacant(entry) => entry.insert(subscriber),
|
return request.reject(err.code());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = self.run_publisher(request, publisher).await {
|
if let Err(err) = self.run_publisher(request, broadcast).await {
|
||||||
log::warn!("error serving pubisher: id={} path={} err={:?}", id, path, err);
|
log::warn!("error serving publisher: id={} path={} err={:#?}", id, path, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.broadcasts.lock().unwrap().remove(path);
|
// TODO can we do this on drop? Otherwise we might miss it.
|
||||||
|
self.origin.remove_broadcast(path).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_publisher(&mut self, request: Request, publisher: broadcast::Publisher) -> anyhow::Result<()> {
|
async fn run_publisher(&mut self, request: Request, publisher: broadcast::Publisher) -> anyhow::Result<()> {
|
||||||
|
@ -93,15 +94,10 @@ impl Session {
|
||||||
async fn serve_subscriber(&mut self, id: usize, request: Request, path: &str) {
|
async fn serve_subscriber(&mut self, id: usize, request: Request, path: &str) {
|
||||||
log::info!("serving subscriber: id={} path={}", id, path);
|
log::info!("serving subscriber: id={} path={}", id, path);
|
||||||
|
|
||||||
let broadcast = match self.broadcasts.lock().unwrap().get(path) {
|
let broadcast = self.origin.get_broadcast(path);
|
||||||
Some(broadcast) => broadcast.clone(),
|
|
||||||
None => {
|
|
||||||
return request.reject(404);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = self.run_subscriber(request, broadcast).await {
|
if let Err(err) = self.run_subscriber(request, broadcast).await {
|
||||||
log::warn!("error serving subscriber: id={} path={} err={:?}", id, path, err);
|
log::warn!("error serving subscriber: id={} path={} err={:#?}", id, path, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,12 @@ categories = ["multimedia", "network-programming", "web-programming"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.4"
|
bytes = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
anyhow = "1"
|
tokio = { version = "1", features = ["macros", "io-util", "sync"] }
|
||||||
tokio = { version = "1.27", features = ["macros", "io-util", "sync"] }
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
indexmap = "2"
|
indexmap = "2"
|
||||||
|
|
||||||
quinn = "0.10"
|
quinn = "0.10"
|
||||||
webtransport-quinn = "0.5.4"
|
webtransport-quinn = "0.6"
|
||||||
|
#webtransport-quinn = { path = "../../webtransport-rs/webtransport-quinn" }
|
||||||
|
|
|
@ -2,23 +2,21 @@
|
||||||
//!
|
//!
|
||||||
//! The [Publisher] can create tracks, either manually or on request.
|
//! The [Publisher] can create tracks, either manually or on request.
|
||||||
//! It receives all requests by a [Subscriber] for a tracks that don't exist.
|
//! It receives all requests by a [Subscriber] for a tracks that don't exist.
|
||||||
//! The simplest implementation is to close every unknown track with [Error::NotFound].
|
//! The simplest implementation is to close every unknown track with [CacheError::NotFound].
|
||||||
//!
|
//!
|
||||||
//! A [Subscriber] can request tracks by name.
|
//! A [Subscriber] can request tracks by name.
|
||||||
//! If the track already exists, it will be returned.
|
//! If the track already exists, it will be returned.
|
||||||
//! If the track doesn't exist, it will be sent to [Unknown] to be handled.
|
//! If the track doesn't exist, it will be sent to [Unknown] to be handled.
|
||||||
//! A [Subscriber] can be cloned to create multiple subscriptions.
|
//! A [Subscriber] can be cloned to create multiple subscriptions.
|
||||||
//!
|
//!
|
||||||
//! The broadcast is automatically closed with [Error::Closed] when [Publisher] is dropped, or all [Subscriber]s are dropped.
|
//! The broadcast is automatically closed with [CacheError::Closed] when [Publisher] is dropped, or all [Subscriber]s are dropped.
|
||||||
use std::{
|
use std::{
|
||||||
collections::{hash_map, HashMap, VecDeque},
|
collections::{hash_map, HashMap, VecDeque},
|
||||||
fmt,
|
fmt,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Error;
|
use super::{track, CacheError, Watch};
|
||||||
|
|
||||||
use super::{track, Watch};
|
|
||||||
|
|
||||||
/// Create a new broadcast.
|
/// Create a new broadcast.
|
||||||
pub fn new() -> (Publisher, Subscriber) {
|
pub fn new() -> (Publisher, Subscriber) {
|
||||||
|
@ -35,27 +33,27 @@ pub fn new() -> (Publisher, Subscriber) {
|
||||||
struct State {
|
struct State {
|
||||||
tracks: HashMap<String, track::Subscriber>,
|
tracks: HashMap<String, track::Subscriber>,
|
||||||
requested: VecDeque<track::Publisher>,
|
requested: VecDeque<track::Publisher>,
|
||||||
closed: Result<(), Error>,
|
closed: Result<(), CacheError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn get(&self, name: &str) -> Result<Option<track::Subscriber>, Error> {
|
pub fn get(&self, name: &str) -> Result<Option<track::Subscriber>, CacheError> {
|
||||||
// Don't check closed, so we can return from cache.
|
// Don't check closed, so we can return from cache.
|
||||||
Ok(self.tracks.get(name).cloned())
|
Ok(self.tracks.get(name).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, track: track::Subscriber) -> Result<(), Error> {
|
pub fn insert(&mut self, track: track::Subscriber) -> Result<(), CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
|
|
||||||
match self.tracks.entry(track.name.clone()) {
|
match self.tracks.entry(track.name.clone()) {
|
||||||
hash_map::Entry::Occupied(_) => return Err(Error::Duplicate),
|
hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate),
|
||||||
hash_map::Entry::Vacant(v) => v.insert(track),
|
hash_map::Entry::Vacant(v) => v.insert(track),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(&mut self, name: &str) -> Result<track::Subscriber, Error> {
|
pub fn request(&mut self, name: &str) -> Result<track::Subscriber, CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
|
|
||||||
// Create a new track.
|
// Create a new track.
|
||||||
|
@ -70,7 +68,7 @@ impl State {
|
||||||
Ok(subscriber)
|
Ok(subscriber)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_next(&self) -> Result<bool, Error> {
|
pub fn has_next(&self) -> Result<bool, CacheError> {
|
||||||
// Check if there's any elements in the queue before checking closed.
|
// Check if there's any elements in the queue before checking closed.
|
||||||
if !self.requested.is_empty() {
|
if !self.requested.is_empty() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
@ -85,7 +83,7 @@ impl State {
|
||||||
self.requested.pop_front().expect("no entry in queue")
|
self.requested.pop_front().expect("no entry in queue")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&mut self, err: Error) -> Result<(), Error> {
|
pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
self.closed = Err(err);
|
self.closed = Err(err);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -117,19 +115,19 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new track with the given name, inserting it into the broadcast.
|
/// Create a new track with the given name, inserting it into the broadcast.
|
||||||
pub fn create_track(&mut self, name: &str) -> Result<track::Publisher, Error> {
|
pub fn create_track(&mut self, name: &str) -> Result<track::Publisher, CacheError> {
|
||||||
let (publisher, subscriber) = track::new(name);
|
let (publisher, subscriber) = track::new(name);
|
||||||
self.state.lock_mut().insert(subscriber)?;
|
self.state.lock_mut().insert(subscriber)?;
|
||||||
Ok(publisher)
|
Ok(publisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a track into the broadcast.
|
/// Insert a track into the broadcast.
|
||||||
pub fn insert_track(&mut self, track: track::Subscriber) -> Result<(), Error> {
|
pub fn insert_track(&mut self, track: track::Subscriber) -> Result<(), CacheError> {
|
||||||
self.state.lock_mut().insert(track)
|
self.state.lock_mut().insert(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the next track requested by a subscriber.
|
/// Block until the next track requested by a subscriber.
|
||||||
pub async fn next_track(&mut self) -> Result<Option<track::Publisher>, Error> {
|
pub async fn next_track(&mut self) -> Result<Option<track::Publisher>, CacheError> {
|
||||||
loop {
|
loop {
|
||||||
let notify = {
|
let notify = {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
@ -145,7 +143,7 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the broadcast with an error.
|
/// Close the broadcast with an error.
|
||||||
pub fn close(self, err: Error) -> Result<(), Error> {
|
pub fn close(self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.state.lock_mut().close(err)
|
self.state.lock_mut().close(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,8 +171,8 @@ impl Subscriber {
|
||||||
|
|
||||||
/// Get a track from the broadcast by name.
|
/// Get a track from the broadcast by name.
|
||||||
/// If the track does not exist, it will be created and potentially fufilled by the publisher (via Unknown).
|
/// If the track does not exist, it will be created and potentially fufilled by the publisher (via Unknown).
|
||||||
/// Otherwise, it will return [Error::NotFound].
|
/// Otherwise, it will return [CacheError::NotFound].
|
||||||
pub fn get_track(&self, name: &str) -> Result<track::Subscriber, Error> {
|
pub fn get_track(&self, name: &str) -> Result<track::Subscriber, CacheError> {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
if let Some(track) = state.get(name)? {
|
if let Some(track) = state.get(name)? {
|
||||||
return Ok(track);
|
return Ok(track);
|
||||||
|
@ -183,6 +181,11 @@ impl Subscriber {
|
||||||
// Request a new track if it does not exist.
|
// Request a new track if it does not exist.
|
||||||
state.into_mut().request(name)
|
state.into_mut().request(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return if the broadcast is closed, either because the publisher was dropped or called [Publisher::close].
|
||||||
|
pub fn closed(&self) -> Option<CacheError> {
|
||||||
|
self.state.lock().closed.as_ref().err().cloned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Subscriber {
|
impl fmt::Debug for Subscriber {
|
||||||
|
@ -206,6 +209,6 @@ impl Dropped {
|
||||||
|
|
||||||
impl Drop for Dropped {
|
impl Drop for Dropped {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.state.lock_mut().close(Error::Closed).ok();
|
self.state.lock_mut().close(CacheError::Closed).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::MoqError;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
pub enum CacheError {
|
||||||
|
/// A clean termination, represented as error code 0.
|
||||||
|
/// This error is automatically used when publishers or subscribers are dropped without calling close.
|
||||||
|
#[error("closed")]
|
||||||
|
Closed,
|
||||||
|
|
||||||
|
/// An ANNOUNCE_RESET or SUBSCRIBE_RESET was sent by the publisher.
|
||||||
|
#[error("reset code={0:?}")]
|
||||||
|
Reset(u32),
|
||||||
|
|
||||||
|
/// An ANNOUNCE_STOP or SUBSCRIBE_STOP was sent by the subscriber.
|
||||||
|
#[error("stop")]
|
||||||
|
Stop,
|
||||||
|
|
||||||
|
/// The requested resource was not found.
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// A resource already exists with that ID.
|
||||||
|
#[error("duplicate")]
|
||||||
|
Duplicate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MoqError for CacheError {
|
||||||
|
/// An integer code that is sent over the wire.
|
||||||
|
fn code(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::Closed => 0,
|
||||||
|
Self::Reset(code) => *code,
|
||||||
|
Self::Stop => 206,
|
||||||
|
Self::NotFound => 404,
|
||||||
|
Self::Duplicate => 409,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reason that is sent over the wire.
|
||||||
|
fn reason(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Closed => "closed",
|
||||||
|
Self::Reset(_) => "reset",
|
||||||
|
Self::Stop => "stop",
|
||||||
|
Self::NotFound => "not found",
|
||||||
|
Self::Duplicate => "duplicate",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,11 @@
|
||||||
//! The hierarchy is: [broadcast] -> [track] -> [segment] -> [Bytes](bytes::Bytes)
|
//! The hierarchy is: [broadcast] -> [track] -> [segment] -> [Bytes](bytes::Bytes)
|
||||||
|
|
||||||
pub mod broadcast;
|
pub mod broadcast;
|
||||||
|
mod error;
|
||||||
pub mod segment;
|
pub mod segment;
|
||||||
pub mod track;
|
pub mod track;
|
||||||
|
|
||||||
pub(crate) mod watch;
|
pub(crate) mod watch;
|
||||||
pub(crate) use watch::*;
|
pub(crate) use watch::*;
|
||||||
|
|
||||||
|
pub use error::*;
|
|
@ -7,14 +7,14 @@
|
||||||
//! These chunks are returned directly from the QUIC connection, so they may be of any size or position.
|
//! These chunks are returned directly from the QUIC connection, so they may be of any size or position.
|
||||||
//! A closed [Subscriber] will receive a copy of all future chunks. (fanout)
|
//! A closed [Subscriber] will receive a copy of all future chunks. (fanout)
|
||||||
//!
|
//!
|
||||||
//! The segment is closed with [Error::Closed] when all publishers or subscribers are dropped.
|
//! The segment is closed with [CacheError::Closed] when all publishers or subscribers are dropped.
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use std::{ops::Deref, sync::Arc, time};
|
use std::{ops::Deref, sync::Arc, time};
|
||||||
|
|
||||||
use crate::{Error, VarInt};
|
use crate::VarInt;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
use super::Watch;
|
use super::{CacheError, Watch};
|
||||||
|
|
||||||
/// Create a new segment with the given info.
|
/// Create a new segment with the given info.
|
||||||
pub fn new(info: Info) -> (Publisher, Subscriber) {
|
pub fn new(info: Info) -> (Publisher, Subscriber) {
|
||||||
|
@ -45,11 +45,11 @@ struct State {
|
||||||
data: Vec<Bytes>,
|
data: Vec<Bytes>,
|
||||||
|
|
||||||
// Set when the publisher is dropped.
|
// Set when the publisher is dropped.
|
||||||
closed: Result<(), Error>,
|
closed: Result<(), CacheError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn close(&mut self, err: Error) -> Result<(), Error> {
|
pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
self.closed = Err(err);
|
self.closed = Err(err);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -97,7 +97,7 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a new chunk of bytes.
|
/// Write a new chunk of bytes.
|
||||||
pub fn write_chunk(&mut self, data: Bytes) -> Result<(), Error> {
|
pub fn write_chunk(&mut self, data: Bytes) -> Result<(), CacheError> {
|
||||||
let mut state = self.state.lock_mut();
|
let mut state = self.state.lock_mut();
|
||||||
state.closed.clone()?;
|
state.closed.clone()?;
|
||||||
state.data.push(data);
|
state.data.push(data);
|
||||||
|
@ -105,7 +105,7 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the segment with an error.
|
/// Close the segment with an error.
|
||||||
pub fn close(self, err: Error) -> Result<(), Error> {
|
pub fn close(self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.state.lock_mut().close(err)
|
self.state.lock_mut().close(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,7 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the next chunk of bytes is available.
|
/// Block until the next chunk of bytes is available.
|
||||||
pub async fn read_chunk(&mut self) -> Result<Option<Bytes>, Error> {
|
pub async fn read_chunk(&mut self) -> Result<Option<Bytes>, CacheError> {
|
||||||
loop {
|
loop {
|
||||||
let notify = {
|
let notify = {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
@ -168,7 +168,7 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
|
|
||||||
match &state.closed {
|
match &state.closed {
|
||||||
Err(Error::Closed) => return Ok(None),
|
Err(CacheError::Closed) => return Ok(None),
|
||||||
Err(err) => return Err(err.clone()),
|
Err(err) => return Err(err.clone()),
|
||||||
Ok(()) => state.changed(),
|
Ok(()) => state.changed(),
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,6 @@ impl Dropped {
|
||||||
|
|
||||||
impl Drop for Dropped {
|
impl Drop for Dropped {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.state.lock_mut().close(Error::Closed).ok();
|
self.state.lock_mut().close(CacheError::Closed).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,14 +10,14 @@
|
||||||
//! Segments will be cached for a potentially limited duration added to the unreliable nature.
|
//! Segments will be cached for a potentially limited duration added to the unreliable nature.
|
||||||
//! A cloned [Subscriber] will receive a copy of all new segment going forward (fanout).
|
//! A cloned [Subscriber] will receive a copy of all new segment going forward (fanout).
|
||||||
//!
|
//!
|
||||||
//! The track is closed with [Error::Closed] when all publishers or subscribers are dropped.
|
//! The track is closed with [CacheError::Closed] when all publishers or subscribers are dropped.
|
||||||
|
|
||||||
use std::{collections::BinaryHeap, fmt, ops::Deref, sync::Arc, time};
|
use std::{collections::BinaryHeap, fmt, ops::Deref, sync::Arc, time};
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use super::{segment, Watch};
|
use super::{segment, CacheError, Watch};
|
||||||
use crate::{Error, VarInt};
|
use crate::VarInt;
|
||||||
|
|
||||||
/// Create a track with the given name.
|
/// Create a track with the given name.
|
||||||
pub fn new(name: &str) -> (Publisher, Subscriber) {
|
pub fn new(name: &str) -> (Publisher, Subscriber) {
|
||||||
|
@ -49,21 +49,21 @@ struct State {
|
||||||
pruned: usize,
|
pruned: usize,
|
||||||
|
|
||||||
// Set when the publisher is closed/dropped, or all subscribers are dropped.
|
// Set when the publisher is closed/dropped, or all subscribers are dropped.
|
||||||
closed: Result<(), Error>,
|
closed: Result<(), CacheError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn close(&mut self, err: Error) -> Result<(), Error> {
|
pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
self.closed = Err(err);
|
self.closed = Err(err);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, segment: segment::Subscriber) -> Result<(), Error> {
|
pub fn insert(&mut self, segment: segment::Subscriber) -> Result<(), CacheError> {
|
||||||
self.closed.clone()?;
|
self.closed.clone()?;
|
||||||
|
|
||||||
let entry = match self.lookup.entry(segment.sequence) {
|
let entry = match self.lookup.entry(segment.sequence) {
|
||||||
indexmap::map::Entry::Occupied(_entry) => return Err(Error::Duplicate),
|
indexmap::map::Entry::Occupied(_entry) => return Err(CacheError::Duplicate),
|
||||||
indexmap::map::Entry::Vacant(entry) => entry,
|
indexmap::map::Entry::Vacant(entry) => entry,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,19 +144,19 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a new segment.
|
/// Insert a new segment.
|
||||||
pub fn insert_segment(&mut self, segment: segment::Subscriber) -> Result<(), Error> {
|
pub fn insert_segment(&mut self, segment: segment::Subscriber) -> Result<(), CacheError> {
|
||||||
self.state.lock_mut().insert(segment)
|
self.state.lock_mut().insert(segment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an insert a segment with the given info.
|
/// Create an insert a segment with the given info.
|
||||||
pub fn create_segment(&mut self, info: segment::Info) -> Result<segment::Publisher, Error> {
|
pub fn create_segment(&mut self, info: segment::Info) -> Result<segment::Publisher, CacheError> {
|
||||||
let (publisher, subscriber) = segment::new(info);
|
let (publisher, subscriber) = segment::new(info);
|
||||||
self.insert_segment(subscriber)?;
|
self.insert_segment(subscriber)?;
|
||||||
Ok(publisher)
|
Ok(publisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the segment with an error.
|
/// Close the segment with an error.
|
||||||
pub fn close(self, err: Error) -> Result<(), Error> {
|
pub fn close(self, err: CacheError) -> Result<(), CacheError> {
|
||||||
self.state.lock_mut().close(err)
|
self.state.lock_mut().close(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,8 +206,8 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the next segment arrives, or return None if the track is [Error::Closed].
|
/// Block until the next segment arrives, or return None if the track is [CacheError::Closed].
|
||||||
pub async fn next_segment(&mut self) -> Result<Option<segment::Subscriber>, Error> {
|
pub async fn next_segment(&mut self) -> Result<Option<segment::Subscriber>, CacheError> {
|
||||||
loop {
|
loop {
|
||||||
let notify = {
|
let notify = {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
@ -237,7 +237,7 @@ impl Subscriber {
|
||||||
|
|
||||||
// Otherwise check if we need to return an error.
|
// Otherwise check if we need to return an error.
|
||||||
match &state.closed {
|
match &state.closed {
|
||||||
Err(Error::Closed) => return Ok(None),
|
Err(CacheError::Closed) => return Ok(None),
|
||||||
Err(err) => return Err(err.clone()),
|
Err(err) => return Err(err.clone()),
|
||||||
Ok(()) => state.changed(),
|
Ok(()) => state.changed(),
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ impl Dropped {
|
||||||
|
|
||||||
impl Drop for Dropped {
|
impl Drop for Dropped {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.state.lock_mut().close(Error::Closed).ok();
|
self.state.lock_mut().close(CacheError::Closed).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
|
use crate::VarInt;
|
||||||
|
|
||||||
|
use super::{AsyncRead, AsyncWrite, DecodeError, EncodeError};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
// I hate this parameter encoding so much.
|
||||||
|
// i hate it i hate it i hate it
|
||||||
|
|
||||||
|
// TODO Use #[async_trait] so we can do Param<VarInt> instead.
|
||||||
|
pub struct ParamInt(pub VarInt);
|
||||||
|
|
||||||
|
impl ParamInt {
|
||||||
|
pub async fn decode<R: AsyncRead>(r: &mut R) -> Result<Self, DecodeError> {
|
||||||
|
// Why do we have a redundant size in front of each VarInt?
|
||||||
|
let size = VarInt::decode(r).await?;
|
||||||
|
let mut take = r.take(size.into_inner());
|
||||||
|
let value = VarInt::decode(&mut take).await?;
|
||||||
|
|
||||||
|
// Like seriously why do I have to check if the VarInt length mismatches.
|
||||||
|
if take.limit() != 0 {
|
||||||
|
return Err(DecodeError::InvalidSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn encode<W: AsyncWrite>(&self, w: &mut W) -> Result<(), EncodeError> {
|
||||||
|
// Seriously why do I have to compute the size.
|
||||||
|
let size = self.0.size();
|
||||||
|
VarInt::try_from(size)?.encode(w).await?;
|
||||||
|
|
||||||
|
self.0.encode(w).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParamBytes(pub Vec<u8>);
|
||||||
|
|
||||||
|
impl ParamBytes {
|
||||||
|
pub async fn decode<R: AsyncRead>(r: &mut R) -> Result<Self, DecodeError> {
|
||||||
|
let size = VarInt::decode(r).await?;
|
||||||
|
let mut take = r.take(size.into_inner());
|
||||||
|
let mut buf = Vec::with_capacity(min(take.limit() as usize, 1024));
|
||||||
|
take.read_to_end(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(Self(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn encode<W: AsyncWrite>(&self, w: &mut W) -> Result<(), EncodeError> {
|
||||||
|
let size = VarInt::try_from(self.0.len())?;
|
||||||
|
size.encode(w).await?;
|
||||||
|
w.write_all(&self.0).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParamUnknown {}
|
||||||
|
|
||||||
|
impl ParamUnknown {
|
||||||
|
pub async fn decode<R: AsyncRead>(r: &mut R) -> Result<(), DecodeError> {
|
||||||
|
// Really? Is there no way to advance without reading?
|
||||||
|
ParamBytes::decode(r).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,82 +1,5 @@
|
||||||
use thiserror::Error;
|
pub trait MoqError {
|
||||||
|
|
||||||
use crate::VarInt;
|
|
||||||
|
|
||||||
/// A MoQTransport error with an associated error code.
|
|
||||||
#[derive(Clone, Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
/// A clean termination, represented as error code 0.
|
|
||||||
/// This error is automatically used when publishers or subscribers are dropped without calling close.
|
|
||||||
#[error("closed")]
|
|
||||||
Closed,
|
|
||||||
|
|
||||||
/// A session error occured.
|
|
||||||
#[error("session error: {0}")]
|
|
||||||
Session(#[from] webtransport_quinn::SessionError),
|
|
||||||
|
|
||||||
/// An ANNOUNCE_RESET or SUBSCRIBE_RESET was sent by the publisher.
|
|
||||||
#[error("reset code={0:?}")]
|
|
||||||
Reset(u32),
|
|
||||||
|
|
||||||
/// An ANNOUNCE_STOP or SUBSCRIBE_STOP was sent by the subscriber.
|
|
||||||
#[error("stop")]
|
|
||||||
Stop,
|
|
||||||
|
|
||||||
/// The requested resource was not found.
|
|
||||||
#[error("not found")]
|
|
||||||
NotFound,
|
|
||||||
|
|
||||||
/// A resource already exists with that ID.
|
|
||||||
#[error("duplicate")]
|
|
||||||
Duplicate,
|
|
||||||
|
|
||||||
/// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT.
|
|
||||||
#[error("role violation: msg={0}")]
|
|
||||||
Role(VarInt),
|
|
||||||
|
|
||||||
/// An error occured while reading from the QUIC stream.
|
|
||||||
#[error("failed to read from stream: {0}")]
|
|
||||||
Read(#[from] webtransport_quinn::ReadError),
|
|
||||||
|
|
||||||
/// An error occured while writing to the QUIC stream.
|
|
||||||
#[error("failed to write to stream: {0}")]
|
|
||||||
Write(#[from] webtransport_quinn::WriteError),
|
|
||||||
|
|
||||||
/// An unclassified error because I'm lazy. TODO classify these errors
|
|
||||||
#[error("unknown error: {0}")]
|
|
||||||
Unknown(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
/// An integer code that is sent over the wire.
|
/// An integer code that is sent over the wire.
|
||||||
pub fn code(&self) -> u32 {
|
fn code(&self) -> u32;
|
||||||
match self {
|
fn reason(&self) -> &str;
|
||||||
Self::Closed => 0,
|
|
||||||
Self::Reset(code) => *code,
|
|
||||||
Self::Stop => 206,
|
|
||||||
Self::NotFound => 404,
|
|
||||||
Self::Role(_) => 405,
|
|
||||||
Self::Duplicate => 409,
|
|
||||||
Self::Unknown(_) => 500,
|
|
||||||
Self::Write(_) => 501,
|
|
||||||
Self::Read(_) => 502,
|
|
||||||
Self::Session(_) => 503,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A reason that is sent over the wire.
|
|
||||||
pub fn reason(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Closed => "closed",
|
|
||||||
Self::Reset(_) => "reset",
|
|
||||||
Self::Stop => "stop",
|
|
||||||
Self::NotFound => "not found",
|
|
||||||
Self::Duplicate => "duplicate",
|
|
||||||
Self::Role(_) => "role violation",
|
|
||||||
Self::Read(_) => "read error",
|
|
||||||
Self::Write(_) => "write error",
|
|
||||||
Self::Session(_) => "session error",
|
|
||||||
Self::Unknown(_) => "unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
mod coding;
|
mod coding;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
|
pub mod cache;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod model;
|
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
pub use coding::VarInt;
|
pub use coding::VarInt;
|
||||||
pub use error::*;
|
pub use error::MoqError;
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
use super::{Publisher, Subscriber};
|
use super::{Publisher, SessionError, Subscriber};
|
||||||
use crate::{model::broadcast, setup};
|
use crate::{cache::broadcast, setup};
|
||||||
use webtransport_quinn::{RecvStream, SendStream, Session};
|
use webtransport_quinn::{RecvStream, SendStream, Session};
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
|
|
||||||
/// An endpoint that connects to a URL to publish and/or consume live streams.
|
/// An endpoint that connects to a URL to publish and/or consume live streams.
|
||||||
pub struct Client {}
|
pub struct Client {}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Connect using an established WebTransport session, performing the MoQ handshake as a publisher.
|
/// Connect using an established WebTransport session, performing the MoQ handshake as a publisher.
|
||||||
pub async fn publisher(session: Session, source: broadcast::Subscriber) -> anyhow::Result<Publisher> {
|
pub async fn publisher(session: Session, source: broadcast::Subscriber) -> Result<Publisher, SessionError> {
|
||||||
let control = Self::send_setup(&session, setup::Role::Publisher).await?;
|
let control = Self::send_setup(&session, setup::Role::Publisher).await?;
|
||||||
|
|
||||||
let publisher = Publisher::new(session, control, source);
|
let publisher = Publisher::new(session, control, source);
|
||||||
|
@ -17,7 +15,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect using an established WebTransport session, performing the MoQ handshake as a subscriber.
|
/// Connect using an established WebTransport session, performing the MoQ handshake as a subscriber.
|
||||||
pub async fn subscriber(session: Session, source: broadcast::Publisher) -> anyhow::Result<Subscriber> {
|
pub async fn subscriber(session: Session, source: broadcast::Publisher) -> Result<Subscriber, SessionError> {
|
||||||
let control = Self::send_setup(&session, setup::Role::Subscriber).await?;
|
let control = Self::send_setup(&session, setup::Role::Subscriber).await?;
|
||||||
|
|
||||||
let subscriber = Subscriber::new(session, control, source);
|
let subscriber = Subscriber::new(session, control, source);
|
||||||
|
@ -31,30 +29,25 @@ impl Client {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async fn send_setup(session: &Session, role: setup::Role) -> anyhow::Result<(SendStream, RecvStream)> {
|
async fn send_setup(session: &Session, role: setup::Role) -> Result<(SendStream, RecvStream), SessionError> {
|
||||||
let mut control = session.open_bi().await.context("failed to oen bidi stream")?;
|
let mut control = session.open_bi().await?;
|
||||||
|
|
||||||
let client = setup::Client {
|
let client = setup::Client {
|
||||||
role,
|
role,
|
||||||
versions: vec![setup::Version::KIXEL_00].into(),
|
versions: vec![setup::Version::KIXEL_00].into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client.encode(&mut control.0).await?;
|
||||||
.encode(&mut control.0)
|
|
||||||
.await
|
|
||||||
.context("failed to send SETUP CLIENT")?;
|
|
||||||
|
|
||||||
let server = setup::Server::decode(&mut control.1)
|
let server = setup::Server::decode(&mut control.1).await?;
|
||||||
.await
|
|
||||||
.context("failed to read SETUP")?;
|
|
||||||
|
|
||||||
if server.version != setup::Version::KIXEL_00 {
|
if server.version != setup::Version::KIXEL_00 {
|
||||||
anyhow::bail!("unsupported version: {:?}", server.version);
|
return Err(SessionError::Version(Some(server.version)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the server replied with the
|
// Make sure the server replied with the
|
||||||
if !client.role.is_compatible(server.role) {
|
if !client.role.is_compatible(server.role) {
|
||||||
anyhow::bail!("incompatible roles: client={:?} server={:?}", client.role, server.role);
|
return Err(SessionError::RoleIncompatible(client.role, server.role));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(control)
|
Ok(control)
|
||||||
|
|
|
@ -5,7 +5,8 @@ use std::{fmt, sync::Arc};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use webtransport_quinn::{RecvStream, SendStream};
|
use webtransport_quinn::{RecvStream, SendStream};
|
||||||
|
|
||||||
use crate::{message::Message, Error};
|
use super::SessionError;
|
||||||
|
use crate::message::Message;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Control {
|
pub(crate) struct Control {
|
||||||
|
@ -21,22 +22,22 @@ impl Control {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send<T: Into<Message> + fmt::Debug>(&self, msg: T) -> Result<(), Error> {
|
pub async fn send<T: Into<Message> + fmt::Debug>(&self, msg: T) -> Result<(), SessionError> {
|
||||||
let mut stream = self.send.lock().await;
|
let mut stream = self.send.lock().await;
|
||||||
log::info!("sending message: {:?}", msg);
|
log::info!("sending message: {:?}", msg);
|
||||||
msg.into()
|
msg.into()
|
||||||
.encode(&mut *stream)
|
.encode(&mut *stream)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Unknown(e.to_string()))?;
|
.map_err(|e| SessionError::Unknown(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's likely a mistake to call this from two different tasks, but it's easier to just support it.
|
// It's likely a mistake to call this from two different tasks, but it's easier to just support it.
|
||||||
pub async fn recv(&self) -> Result<Message, Error> {
|
pub async fn recv(&self) -> Result<Message, SessionError> {
|
||||||
let mut stream = self.recv.lock().await;
|
let mut stream = self.recv.lock().await;
|
||||||
let msg = Message::decode(&mut *stream)
|
let msg = Message::decode(&mut *stream)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Unknown(e.to_string()))?;
|
.map_err(|e| SessionError::Unknown(e.to_string()))?;
|
||||||
Ok(msg)
|
Ok(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
use crate::{cache, coding, setup, MoqError, VarInt};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum SessionError {
|
||||||
|
#[error("webtransport error: {0}")]
|
||||||
|
Session(#[from] webtransport_quinn::SessionError),
|
||||||
|
|
||||||
|
#[error("cache error: {0}")]
|
||||||
|
Cache(#[from] cache::CacheError),
|
||||||
|
|
||||||
|
#[error("encode error: {0}")]
|
||||||
|
Encode(#[from] coding::EncodeError),
|
||||||
|
|
||||||
|
#[error("decode error: {0}")]
|
||||||
|
Decode(#[from] coding::DecodeError),
|
||||||
|
|
||||||
|
#[error("unsupported version: {0:?}")]
|
||||||
|
Version(Option<setup::Version>),
|
||||||
|
|
||||||
|
#[error("incompatible roles: client={0:?} server={1:?}")]
|
||||||
|
RoleIncompatible(setup::Role, setup::Role),
|
||||||
|
|
||||||
|
/// An error occured while reading from the QUIC stream.
|
||||||
|
#[error("failed to read from stream: {0}")]
|
||||||
|
Read(#[from] webtransport_quinn::ReadError),
|
||||||
|
|
||||||
|
/// An error occured while writing to the QUIC stream.
|
||||||
|
#[error("failed to write to stream: {0}")]
|
||||||
|
Write(#[from] webtransport_quinn::WriteError),
|
||||||
|
|
||||||
|
/// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT.
|
||||||
|
#[error("role violation: msg={0}")]
|
||||||
|
RoleViolation(VarInt),
|
||||||
|
|
||||||
|
/// An unclassified error because I'm lazy. TODO classify these errors
|
||||||
|
#[error("unknown error: {0}")]
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MoqError for SessionError {
|
||||||
|
/// An integer code that is sent over the wire.
|
||||||
|
fn code(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::Cache(err) => err.code(),
|
||||||
|
Self::RoleIncompatible(..) => 406,
|
||||||
|
Self::RoleViolation(..) => 405,
|
||||||
|
Self::Unknown(_) => 500,
|
||||||
|
Self::Write(_) => 501,
|
||||||
|
Self::Read(_) => 502,
|
||||||
|
Self::Session(_) => 503,
|
||||||
|
Self::Version(_) => 406,
|
||||||
|
Self::Encode(_) => 500,
|
||||||
|
Self::Decode(_) => 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reason that is sent over the wire.
|
||||||
|
fn reason(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Cache(err) => err.reason(),
|
||||||
|
Self::RoleViolation(_) => "role violation",
|
||||||
|
Self::RoleIncompatible(..) => "role incompatible",
|
||||||
|
Self::Read(_) => "read error",
|
||||||
|
Self::Write(_) => "write error",
|
||||||
|
Self::Session(_) => "session error",
|
||||||
|
Self::Unknown(_) => "unknown",
|
||||||
|
Self::Version(_) => "unsupported version",
|
||||||
|
Self::Encode(_) => "encode error",
|
||||||
|
Self::Decode(_) => "decode error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,12 +14,14 @@
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod control;
|
mod control;
|
||||||
|
mod error;
|
||||||
mod publisher;
|
mod publisher;
|
||||||
mod server;
|
mod server;
|
||||||
mod subscriber;
|
mod subscriber;
|
||||||
|
|
||||||
pub use client::*;
|
pub use client::*;
|
||||||
pub(crate) use control::*;
|
pub(crate) use control::*;
|
||||||
|
pub use error::*;
|
||||||
pub use publisher::*;
|
pub use publisher::*;
|
||||||
pub use server::*;
|
pub use server::*;
|
||||||
pub use subscriber::*;
|
pub use subscriber::*;
|
||||||
|
|
|
@ -7,13 +7,13 @@ use tokio::task::AbortHandle;
|
||||||
use webtransport_quinn::{RecvStream, SendStream, Session};
|
use webtransport_quinn::{RecvStream, SendStream, Session};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
cache::{broadcast, segment, track, CacheError},
|
||||||
message,
|
message,
|
||||||
message::Message,
|
message::Message,
|
||||||
model::{broadcast, segment, track},
|
MoqError, VarInt,
|
||||||
Error, VarInt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Control;
|
use super::{Control, SessionError};
|
||||||
|
|
||||||
/// Serves broadcasts over the network, automatically handling subscriptions and caching.
|
/// Serves broadcasts over the network, automatically handling subscriptions and caching.
|
||||||
// TODO Clone specific fields when a task actually needs it.
|
// TODO Clone specific fields when a task actually needs it.
|
||||||
|
@ -39,16 +39,30 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Serve a broadcast without sending an ANNOUNCE.
|
// TODO Serve a broadcast without sending an ANNOUNCE.
|
||||||
// fn serve(&mut self, broadcast: broadcast::Subscriber) -> Result<(), Error> {
|
// fn serve(&mut self, broadcast: broadcast::Subscriber) -> Result<(), SessionError> {
|
||||||
|
|
||||||
// TODO Wait until the next subscribe that doesn't route to an ANNOUNCE.
|
// TODO Wait until the next subscribe that doesn't route to an ANNOUNCE.
|
||||||
// pub async fn subscribed(&mut self) -> Result<track::Producer, Error> {
|
// pub async fn subscribed(&mut self) -> Result<track::Producer, SessionError> {
|
||||||
|
|
||||||
pub async fn run(mut self) -> Result<(), Error> {
|
pub async fn run(mut self) -> Result<(), SessionError> {
|
||||||
|
let res = self.run_inner().await;
|
||||||
|
|
||||||
|
// Terminate all active subscribes on error.
|
||||||
|
self.subscribes
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.drain()
|
||||||
|
.for_each(|(_, abort)| abort.abort());
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_inner(&mut self) -> Result<(), SessionError> {
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_stream = self.webtransport.accept_uni() => {
|
stream = self.webtransport.accept_uni() => {
|
||||||
return Err(Error::Role(VarInt::ZERO));
|
stream?;
|
||||||
|
return Err(SessionError::RoleViolation(VarInt::ZERO));
|
||||||
}
|
}
|
||||||
// NOTE: this is not cancel safe, but it's fine since the other branch is a fatal error.
|
// NOTE: this is not cancel safe, but it's fine since the other branch is a fatal error.
|
||||||
msg = self.control.recv() => {
|
msg = self.control.recv() => {
|
||||||
|
@ -63,27 +77,27 @@ impl Publisher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_message(&mut self, msg: &Message) -> Result<(), Error> {
|
async fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> {
|
||||||
match msg {
|
match msg {
|
||||||
Message::AnnounceOk(msg) => self.recv_announce_ok(msg).await,
|
Message::AnnounceOk(msg) => self.recv_announce_ok(msg).await,
|
||||||
Message::AnnounceStop(msg) => self.recv_announce_stop(msg).await,
|
Message::AnnounceStop(msg) => self.recv_announce_stop(msg).await,
|
||||||
Message::Subscribe(msg) => self.recv_subscribe(msg).await,
|
Message::Subscribe(msg) => self.recv_subscribe(msg).await,
|
||||||
Message::SubscribeStop(msg) => self.recv_subscribe_stop(msg).await,
|
Message::SubscribeStop(msg) => self.recv_subscribe_stop(msg).await,
|
||||||
_ => Err(Error::Role(msg.id())),
|
_ => Err(SessionError::RoleViolation(msg.id())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_announce_ok(&mut self, _msg: &message::AnnounceOk) -> Result<(), Error> {
|
async fn recv_announce_ok(&mut self, _msg: &message::AnnounceOk) -> Result<(), SessionError> {
|
||||||
// We didn't send an announce.
|
// We didn't send an announce.
|
||||||
Err(Error::NotFound)
|
Err(CacheError::NotFound.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_announce_stop(&mut self, _msg: &message::AnnounceStop) -> Result<(), Error> {
|
async fn recv_announce_stop(&mut self, _msg: &message::AnnounceStop) -> Result<(), SessionError> {
|
||||||
// We didn't send an announce.
|
// We didn't send an announce.
|
||||||
Err(Error::NotFound)
|
Err(CacheError::NotFound.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_subscribe(&mut self, msg: &message::Subscribe) -> Result<(), Error> {
|
async fn recv_subscribe(&mut self, msg: &message::Subscribe) -> Result<(), SessionError> {
|
||||||
// Assume that the subscribe ID is unique for now.
|
// Assume that the subscribe ID is unique for now.
|
||||||
let abort = match self.start_subscribe(msg.clone()) {
|
let abort = match self.start_subscribe(msg.clone()) {
|
||||||
Ok(abort) => abort,
|
Ok(abort) => abort,
|
||||||
|
@ -92,14 +106,14 @@ impl Publisher {
|
||||||
|
|
||||||
// Insert the abort handle into the lookup table.
|
// Insert the abort handle into the lookup table.
|
||||||
match self.subscribes.lock().unwrap().entry(msg.id) {
|
match self.subscribes.lock().unwrap().entry(msg.id) {
|
||||||
hash_map::Entry::Occupied(_) => return Err(Error::Duplicate), // TODO fatal, because we already started the task
|
hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate.into()), // TODO fatal, because we already started the task
|
||||||
hash_map::Entry::Vacant(entry) => entry.insert(abort),
|
hash_map::Entry::Vacant(entry) => entry.insert(abort),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.control.send(message::SubscribeOk { id: msg.id }).await
|
self.control.send(message::SubscribeOk { id: msg.id }).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reset_subscribe(&mut self, id: VarInt, err: Error) -> Result<(), Error> {
|
async fn reset_subscribe<E: MoqError>(&mut self, id: VarInt, err: E) -> Result<(), SessionError> {
|
||||||
let msg = message::SubscribeReset {
|
let msg = message::SubscribeReset {
|
||||||
id,
|
id,
|
||||||
code: err.code(),
|
code: err.code(),
|
||||||
|
@ -109,10 +123,10 @@ impl Publisher {
|
||||||
self.control.send(msg).await
|
self.control.send(msg).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_subscribe(&mut self, msg: message::Subscribe) -> Result<AbortHandle, Error> {
|
fn start_subscribe(&mut self, msg: message::Subscribe) -> Result<AbortHandle, SessionError> {
|
||||||
// We currently don't use the namespace field in SUBSCRIBE
|
// We currently don't use the namespace field in SUBSCRIBE
|
||||||
if !msg.namespace.is_empty() {
|
if !msg.namespace.is_empty() {
|
||||||
return Err(Error::NotFound);
|
return Err(CacheError::NotFound.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut track = self.source.get_track(&msg.name)?;
|
let mut track = self.source.get_track(&msg.name)?;
|
||||||
|
@ -125,11 +139,11 @@ impl Publisher {
|
||||||
|
|
||||||
let res = this.run_subscribe(msg.id, &mut track).await;
|
let res = this.run_subscribe(msg.id, &mut track).await;
|
||||||
if let Err(err) = &res {
|
if let Err(err) = &res {
|
||||||
log::warn!("failed to serve track: name={} err={:?}", track.name, err);
|
log::warn!("failed to serve track: name={} err={:#?}", track.name, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we send a reset at the end.
|
// Make sure we send a reset at the end.
|
||||||
let err = res.err().unwrap_or(Error::Closed);
|
let err = res.err().unwrap_or(CacheError::Closed.into());
|
||||||
this.reset_subscribe(msg.id, err).await.ok();
|
this.reset_subscribe(msg.id, err).await.ok();
|
||||||
|
|
||||||
// We're all done, so clean up the abort handle.
|
// We're all done, so clean up the abort handle.
|
||||||
|
@ -139,7 +153,7 @@ impl Publisher {
|
||||||
Ok(handle.abort_handle())
|
Ok(handle.abort_handle())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_subscribe(&self, id: VarInt, track: &mut track::Subscriber) -> Result<(), Error> {
|
async fn run_subscribe(&self, id: VarInt, track: &mut track::Subscriber) -> Result<(), SessionError> {
|
||||||
// TODO add an Ok method to track::Publisher so we can send SUBSCRIBE_OK
|
// TODO add an Ok method to track::Publisher so we can send SUBSCRIBE_OK
|
||||||
|
|
||||||
while let Some(mut segment) = track.next_segment().await? {
|
while let Some(mut segment) = track.next_segment().await? {
|
||||||
|
@ -156,7 +170,7 @@ impl Publisher {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_segment(&self, id: VarInt, segment: &mut segment::Subscriber) -> Result<(), Error> {
|
async fn run_segment(&self, id: VarInt, segment: &mut segment::Subscriber) -> Result<(), SessionError> {
|
||||||
let object = message::Object {
|
let object = message::Object {
|
||||||
track: id,
|
track: id,
|
||||||
sequence: segment.sequence,
|
sequence: segment.sequence,
|
||||||
|
@ -172,7 +186,7 @@ impl Publisher {
|
||||||
object
|
object
|
||||||
.encode(&mut stream)
|
.encode(&mut stream)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Unknown(e.to_string()))?;
|
.map_err(|e| SessionError::Unknown(e.to_string()))?;
|
||||||
|
|
||||||
while let Some(data) = segment.read_chunk().await? {
|
while let Some(data) = segment.read_chunk().await? {
|
||||||
stream.write_chunk(data).await?;
|
stream.write_chunk(data).await?;
|
||||||
|
@ -181,10 +195,15 @@ impl Publisher {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_subscribe_stop(&mut self, msg: &message::SubscribeStop) -> Result<(), Error> {
|
async fn recv_subscribe_stop(&mut self, msg: &message::SubscribeStop) -> Result<(), SessionError> {
|
||||||
let abort = self.subscribes.lock().unwrap().remove(&msg.id).ok_or(Error::NotFound)?;
|
let abort = self
|
||||||
|
.subscribes
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&msg.id)
|
||||||
|
.ok_or(CacheError::NotFound)?;
|
||||||
abort.abort();
|
abort.abort();
|
||||||
|
|
||||||
self.reset_subscribe(msg.id, Error::Stop).await
|
self.reset_subscribe(msg.id, CacheError::Stop).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use super::{Publisher, Subscriber};
|
use super::{Publisher, SessionError, Subscriber};
|
||||||
use crate::{model::broadcast, setup};
|
use crate::{cache::broadcast, setup};
|
||||||
|
|
||||||
use webtransport_quinn::{RecvStream, SendStream, Session};
|
use webtransport_quinn::{RecvStream, SendStream, Session};
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
|
|
||||||
/// An endpoint that accepts connections, publishing and/or consuming live streams.
|
/// An endpoint that accepts connections, publishing and/or consuming live streams.
|
||||||
pub struct Server {}
|
pub struct Server {}
|
||||||
|
|
||||||
|
@ -12,18 +10,16 @@ impl Server {
|
||||||
/// Accept an established Webtransport session, performing the MoQ handshake.
|
/// Accept an established Webtransport session, performing the MoQ handshake.
|
||||||
///
|
///
|
||||||
/// This returns a [Request] half-way through the handshake that allows the application to accept or deny the session.
|
/// This returns a [Request] half-way through the handshake that allows the application to accept or deny the session.
|
||||||
pub async fn accept(session: Session) -> anyhow::Result<Request> {
|
pub async fn accept(session: Session) -> Result<Request, SessionError> {
|
||||||
let mut control = session.accept_bi().await.context("failed to accept bidi stream")?;
|
let mut control = session.accept_bi().await?;
|
||||||
|
|
||||||
let client = setup::Client::decode(&mut control.1)
|
let client = setup::Client::decode(&mut control.1).await?;
|
||||||
.await
|
|
||||||
.context("failed to read CLIENT SETUP")?;
|
|
||||||
|
|
||||||
client
|
client
|
||||||
.versions
|
.versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|version| **version == setup::Version::KIXEL_00)
|
.find(|version| **version == setup::Version::KIXEL_00)
|
||||||
.context("no supported versions")?;
|
.ok_or_else(|| SessionError::Version(client.versions.last().cloned()))?;
|
||||||
|
|
||||||
Ok(Request {
|
Ok(Request {
|
||||||
session,
|
session,
|
||||||
|
@ -42,7 +38,7 @@ pub struct Request {
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
/// Accept the session as a publisher, using the provided broadcast to serve subscriptions.
|
/// Accept the session as a publisher, using the provided broadcast to serve subscriptions.
|
||||||
pub async fn publisher(mut self, source: broadcast::Subscriber) -> anyhow::Result<Publisher> {
|
pub async fn publisher(mut self, source: broadcast::Subscriber) -> Result<Publisher, SessionError> {
|
||||||
self.send_setup(setup::Role::Publisher).await?;
|
self.send_setup(setup::Role::Publisher).await?;
|
||||||
|
|
||||||
let publisher = Publisher::new(self.session, self.control, source);
|
let publisher = Publisher::new(self.session, self.control, source);
|
||||||
|
@ -50,7 +46,7 @@ impl Request {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accept the session as a subscriber only.
|
/// Accept the session as a subscriber only.
|
||||||
pub async fn subscriber(mut self, source: broadcast::Publisher) -> anyhow::Result<Subscriber> {
|
pub async fn subscriber(mut self, source: broadcast::Publisher) -> Result<Subscriber, SessionError> {
|
||||||
self.send_setup(setup::Role::Subscriber).await?;
|
self.send_setup(setup::Role::Subscriber).await?;
|
||||||
|
|
||||||
let subscriber = Subscriber::new(self.session, self.control, source);
|
let subscriber = Subscriber::new(self.session, self.control, source);
|
||||||
|
@ -64,7 +60,7 @@ impl Request {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async fn send_setup(&mut self, role: setup::Role) -> anyhow::Result<()> {
|
async fn send_setup(&mut self, role: setup::Role) -> Result<(), SessionError> {
|
||||||
let server = setup::Server {
|
let server = setup::Server {
|
||||||
role,
|
role,
|
||||||
version: setup::Version::KIXEL_00,
|
version: setup::Version::KIXEL_00,
|
||||||
|
@ -73,17 +69,10 @@ impl Request {
|
||||||
// We need to sure we support the opposite of the client's role.
|
// We need to sure we support the opposite of the client's role.
|
||||||
// ex. if the client is a publisher, we must be a subscriber ONLY.
|
// ex. if the client is a publisher, we must be a subscriber ONLY.
|
||||||
if !self.client.role.is_compatible(server.role) {
|
if !self.client.role.is_compatible(server.role) {
|
||||||
anyhow::bail!(
|
return Err(SessionError::RoleIncompatible(self.client.role, server.role));
|
||||||
"incompatible roles: client={:?} server={:?}",
|
|
||||||
self.client.role,
|
|
||||||
server.role
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server
|
server.encode(&mut self.control.0).await?;
|
||||||
.encode(&mut self.control.0)
|
|
||||||
.await
|
|
||||||
.context("failed to send setup server")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,13 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
cache::{broadcast, segment, track, CacheError},
|
||||||
message,
|
message,
|
||||||
message::Message,
|
message::Message,
|
||||||
model::{broadcast, segment, track},
|
session::{Control, SessionError},
|
||||||
Error, VarInt,
|
VarInt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Control;
|
|
||||||
|
|
||||||
/// Receives broadcasts over the network, automatically handling subscriptions and caching.
|
/// Receives broadcasts over the network, automatically handling subscriptions and caching.
|
||||||
// TODO Clone specific fields when a task actually needs it.
|
// TODO Clone specific fields when a task actually needs it.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -47,7 +46,7 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self) -> Result<(), Error> {
|
pub async fn run(self) -> Result<(), SessionError> {
|
||||||
let inbound = self.clone().run_inbound();
|
let inbound = self.clone().run_inbound();
|
||||||
let streams = self.clone().run_streams();
|
let streams = self.clone().run_streams();
|
||||||
let source = self.clone().run_source();
|
let source = self.clone().run_source();
|
||||||
|
@ -60,7 +59,7 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_inbound(mut self) -> Result<(), Error> {
|
async fn run_inbound(mut self) -> Result<(), SessionError> {
|
||||||
loop {
|
loop {
|
||||||
let msg = self.control.recv().await?;
|
let msg = self.control.recv().await?;
|
||||||
|
|
||||||
|
@ -71,28 +70,28 @@ impl Subscriber {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_message(&mut self, msg: &Message) -> Result<(), Error> {
|
async fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Announce(_) => Ok(()), // don't care
|
Message::Announce(_) => Ok(()), // don't care
|
||||||
Message::AnnounceReset(_) => Ok(()), // also don't care
|
Message::AnnounceReset(_) => Ok(()), // also don't care
|
||||||
Message::SubscribeOk(_) => Ok(()), // guess what, don't care
|
Message::SubscribeOk(_) => Ok(()), // guess what, don't care
|
||||||
Message::SubscribeReset(msg) => self.recv_subscribe_reset(msg).await,
|
Message::SubscribeReset(msg) => self.recv_subscribe_reset(msg).await,
|
||||||
Message::GoAway(_msg) => unimplemented!("GOAWAY"),
|
Message::GoAway(_msg) => unimplemented!("GOAWAY"),
|
||||||
_ => Err(Error::Role(msg.id())),
|
_ => Err(SessionError::RoleViolation(msg.id())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_subscribe_reset(&mut self, msg: &message::SubscribeReset) -> Result<(), Error> {
|
async fn recv_subscribe_reset(&mut self, msg: &message::SubscribeReset) -> Result<(), SessionError> {
|
||||||
let err = Error::Reset(msg.code);
|
let err = CacheError::Reset(msg.code);
|
||||||
|
|
||||||
let mut subscribes = self.subscribes.lock().unwrap();
|
let mut subscribes = self.subscribes.lock().unwrap();
|
||||||
let subscribe = subscribes.remove(&msg.id).ok_or(Error::NotFound)?;
|
let subscribe = subscribes.remove(&msg.id).ok_or(CacheError::NotFound)?;
|
||||||
subscribe.close(err)?;
|
subscribe.close(err)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_streams(self) -> Result<(), Error> {
|
async fn run_streams(self) -> Result<(), SessionError> {
|
||||||
loop {
|
loop {
|
||||||
// Accept all incoming unidirectional streams.
|
// Accept all incoming unidirectional streams.
|
||||||
let stream = self.webtransport.accept_uni().await?;
|
let stream = self.webtransport.accept_uni().await?;
|
||||||
|
@ -100,24 +99,24 @@ impl Subscriber {
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(err) = this.run_stream(stream).await {
|
if let Err(err) = this.run_stream(stream).await {
|
||||||
log::warn!("failed to receive stream: err={:?}", err);
|
log::warn!("failed to receive stream: err={:#?}", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_stream(self, mut stream: RecvStream) -> Result<(), Error> {
|
async fn run_stream(self, mut stream: RecvStream) -> Result<(), SessionError> {
|
||||||
// Decode the object on the data stream.
|
// Decode the object on the data stream.
|
||||||
let object = message::Object::decode(&mut stream)
|
let object = message::Object::decode(&mut stream)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Unknown(e.to_string()))?;
|
.map_err(|e| SessionError::Unknown(e.to_string()))?;
|
||||||
|
|
||||||
log::debug!("received object: {:?}", object);
|
log::debug!("received object: {:?}", object);
|
||||||
|
|
||||||
// A new scope is needed because the async compiler is dumb
|
// A new scope is needed because the async compiler is dumb
|
||||||
let mut publisher = {
|
let mut publisher = {
|
||||||
let mut subscribes = self.subscribes.lock().unwrap();
|
let mut subscribes = self.subscribes.lock().unwrap();
|
||||||
let track = subscribes.get_mut(&object.track).ok_or(Error::NotFound)?;
|
let track = subscribes.get_mut(&object.track).ok_or(CacheError::NotFound)?;
|
||||||
|
|
||||||
track.create_segment(segment::Info {
|
track.create_segment(segment::Info {
|
||||||
sequence: object.sequence,
|
sequence: object.sequence,
|
||||||
|
@ -127,13 +126,15 @@ impl Subscriber {
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(data) = stream.read_chunk(usize::MAX, true).await? {
|
while let Some(data) = stream.read_chunk(usize::MAX, true).await? {
|
||||||
|
// NOTE: This does not make a copy!
|
||||||
|
// Bytes are immutable and ref counted.
|
||||||
publisher.write_chunk(data.bytes)?;
|
publisher.write_chunk(data.bytes)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_source(mut self) -> Result<(), Error> {
|
async fn run_source(mut self) -> Result<(), SessionError> {
|
||||||
while let Some(track) = self.source.next_track().await? {
|
while let Some(track) = self.source.next_track().await? {
|
||||||
let name = track.name.clone();
|
let name = track.name.clone();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue