diff --git a/.dockerignore b/.dockerignore
index 1611ea0..a7ed94c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,2 @@
-media/*.mp4
-target/*
-cert/*
+target
+dev
diff --git a/.github/logo.svg b/.github/logo.svg
new file mode 100644
index 0000000..109b070
--- /dev/null
+++ b/.github/logo.svg
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Cargo.lock b/Cargo.lock
index 10b8c38..5cdc8fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
-version = "0.20.0"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
@@ -19,39 +19,38 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
-version = "1.0.2"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
-version = "0.3.2"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
- "is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
-version = "1.0.0"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
[[package]]
name = "anstyle-parse"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
@@ -62,24 +61,24 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
name = "anstyle-wincon"
-version = "1.0.1"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [
"anstyle",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
name = "anyhow"
-version = "1.0.71"
+version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
dependencies = [
"backtrace",
]
@@ -146,9 +145,9 @@ dependencies = [
[[package]]
name = "async-lock"
-version = "2.7.0"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [
"event-listener",
]
@@ -210,9 +209,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
-version = "0.3.68"
+version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
@@ -231,9 +230,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
-version = "0.21.2"
+version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
[[package]]
name = "bitflags"
@@ -279,15 +278,18 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cc"
-version = "1.0.79"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
[[package]]
name = "cfg-if"
@@ -297,33 +299,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
-version = "4.3.4"
+version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed"
+checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
dependencies = [
"clap_builder",
"clap_derive",
- "once_cell",
]
[[package]]
name = "clap_builder"
-version = "4.3.4"
+version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636"
+checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstream",
"anstyle",
- "bitflags",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
-version = "4.3.2"
+version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
+checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
dependencies = [
"heck",
"proc-macro2",
@@ -333,15 +333,15 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "clap_mangen"
-version = "0.2.12"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f2e32b579dae093c2424a8b7e2bea09c89da01e1ce5065eb2f0a6f1cc15cc1f"
+checksum = "cf8e5f34d85d9e0bbe2491d100a7a7c1007bb2467b518080bfe311e8947197a9"
dependencies = [
"clap",
"roff",
@@ -380,9 +380,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
-version = "0.2.8"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "encoding_rs"
-version = "0.8.32"
+version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
@@ -439,14 +439,20 @@ dependencies = [
]
[[package]]
-name = "errno"
-version = "0.3.1"
+name = "equivalent"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -622,9 +628,9 @@ dependencies = [
[[package]]
name = "gimli"
-version = "0.27.3"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "gloo-timers"
@@ -640,9 +646,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.19"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
"bytes",
"fnv",
@@ -650,7 +656,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
- "indexmap",
+ "indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@@ -664,13 +670,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
-name = "headers"
-version = "0.3.8"
+name = "hashbrown"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "headers"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
dependencies = [
- "base64 0.13.1",
- "bitflags",
+ "base64 0.21.4",
"bytes",
"headers-core",
"http",
@@ -705,18 +716,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
-version = "0.2.6"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
@@ -754,9 +756,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
@@ -766,9 +768,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
-version = "0.14.26"
+version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
@@ -805,7 +807,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.0",
]
[[package]]
@@ -823,28 +835,16 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
- "hermit-abi 0.3.1",
+ "hermit-abi 0.3.2",
"libc",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "is-terminal"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
-dependencies = [
- "hermit-abi 0.3.1",
- "io-lifetimes",
- "rustix",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
name = "itoa"
-version = "1.0.6"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
@@ -865,10 +865,16 @@ dependencies = [
]
[[package]]
-name = "libc"
-version = "0.2.146"
+name = "lazy_static"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "linux-raw-sys"
@@ -888,18 +894,18 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.19"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
dependencies = [
"value-bag",
]
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "mime"
@@ -934,7 +940,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -948,23 +954,21 @@ dependencies = [
"http",
"log",
"moq-transport",
- "moq-warp",
"mp4",
"quinn",
"rfc6381-codec",
"ring",
- "rustls 0.21.2",
+ "rustls 0.21.7",
"rustls-native-certs",
"rustls-pemfile",
"serde_json",
"tokio",
- "uuid",
"webtransport-generic",
"webtransport-quinn",
]
[[package]]
-name = "moq-quinn"
+name = "moq-relay"
version = "0.1.0"
dependencies = [
"anyhow",
@@ -973,12 +977,13 @@ dependencies = [
"hex",
"log",
"moq-transport",
- "moq-warp",
"quinn",
"ring",
- "rustls 0.21.2",
+ "rustls 0.21.7",
"rustls-pemfile",
"tokio",
+ "tracing",
+ "tracing-subscriber",
"warp",
"webtransport-generic",
"webtransport-quinn",
@@ -986,25 +991,16 @@ dependencies = [
[[package]]
name = "moq-transport"
-version = "0.1.0"
+version = "0.2.0"
dependencies = [
"anyhow",
"bytes",
+ "indexmap 2.0.0",
+ "log",
+ "quinn",
"thiserror",
"tokio",
- "webtransport-generic",
-]
-
-[[package]]
-name = "moq-warp"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "bytes",
- "log",
- "moq-transport",
- "tokio",
- "webtransport-generic",
+ "webtransport-quinn",
]
[[package]]
@@ -1055,10 +1051,20 @@ dependencies = [
]
[[package]]
-name = "num-bigint"
-version = "0.4.3"
+name = "nu-ansi-term"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [
"autocfg",
"num-integer",
@@ -1099,19 +1105,19 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
- "hermit-abi 0.2.6",
+ "hermit-abi 0.3.2",
"libc",
]
[[package]]
name = "object"
-version = "0.31.1"
+version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
dependencies = [
"memchr",
]
@@ -1128,6 +1134,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
[[package]]
name = "parking"
version = "2.1.0"
@@ -1165,18 +1177,18 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pin-project"
-version = "1.1.0"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.0"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
@@ -1185,9 +1197,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.9"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@@ -1208,7 +1220,7 @@ dependencies = [
"libc",
"log",
"pin-project-lite",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -1228,16 +1240,16 @@ dependencies = [
[[package]]
name = "quinn"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21252f1c0fc131f1b69182db8f34837e8a69737b8251dff75636a9be0518c324"
+checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
- "rustls 0.21.2",
+ "rustls 0.21.7",
"thiserror",
"tokio",
"tracing",
@@ -1245,15 +1257,15 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.10.1"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85af4ed6ee5a89f26a26086e9089a6643650544c025158449a3626ebf72884b3"
+checksum = "e13f81c9a9d574310b8351f8666f5a93ac3b0069c45c28ad52c10291389a7cf9"
dependencies = [
"bytes",
"rand",
"ring",
"rustc-hash",
- "rustls 0.21.2",
+ "rustls 0.21.7",
"rustls-native-certs",
"slab",
"thiserror",
@@ -1263,22 +1275,22 @@ dependencies = [
[[package]]
name = "quinn-udp"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6df19e284d93757a9fb91d63672f7741b129246a669db09d1c0063071debc0c0"
+checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7"
dependencies = [
"bytes",
"libc",
- "socket2 0.5.3",
+ "socket2 0.5.4",
"tracing",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
name = "quote"
-version = "1.0.28"
+version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
@@ -1324,9 +1336,21 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.8.4"
+version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
+checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
@@ -1335,9 +1359,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.7.2"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rfc6381-codec"
@@ -1385,23 +1409,23 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
-version = "0.37.20"
+version = "0.37.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
+checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
name = "rustls"
-version = "0.20.8"
+version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
+checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
dependencies = [
"log",
"ring",
@@ -1411,9 +1435,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.21.2"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
+checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"log",
"ring",
@@ -1435,18 +1459,18 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
- "base64 0.21.2",
+ "base64 0.21.4",
]
[[package]]
name = "rustls-webpki"
-version = "0.100.2"
+version = "0.101.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab"
+checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
dependencies = [
"ring",
"untrusted",
@@ -1454,17 +1478,17 @@ dependencies = [
[[package]]
name = "ryu"
-version = "1.0.13"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "schannel"
-version = "0.1.21"
+version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
- "windows-sys 0.42.0",
+ "windows-sys",
]
[[package]]
@@ -1475,9 +1499,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
@@ -1491,9 +1515,9 @@ dependencies = [
[[package]]
name = "security-framework"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags",
"core-foundation",
@@ -1504,9 +1528,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.9.0"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
@@ -1534,9 +1558,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.105"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
+checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [
"itoa",
"ryu",
@@ -1566,6 +1590,15 @@ dependencies = [
"digest",
]
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -1577,18 +1610,18 @@ dependencies = [
[[package]]
name = "slab"
-version = "0.4.8"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "socket2"
@@ -1602,12 +1635,12 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.5.3"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
dependencies = [
"libc",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -1630,9 +1663,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
-version = "2.0.29"
+version = "2.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
+checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
dependencies = [
"proc-macro2",
"quote",
@@ -1650,24 +1683,34 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.40"
+version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.40"
+version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"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]]
name = "tinyvec"
version = "1.6.0"
@@ -1685,11 +1728,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.29.1"
+version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
- "autocfg",
"backtrace",
"bytes",
"libc",
@@ -1698,9 +1740,9 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.4.9",
+ "socket2 0.5.4",
"tokio-macros",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -1720,7 +1762,7 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
- "rustls 0.20.8",
+ "rustls 0.20.9",
"tokio",
"webpki",
]
@@ -1783,9 +1825,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.24"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
@@ -1799,6 +1841,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.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]]
@@ -1834,9 +1902,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicase"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
@@ -1849,9 +1917,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
-version = "1.0.9"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-normalization"
@@ -1870,9 +1938,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
-version = "2.4.0"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
"idna",
@@ -1892,26 +1960,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
-name = "uuid"
-version = "1.4.1"
+name = "valuable"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
-dependencies = [
- "getrandom",
- "rand",
- "uuid-macro-internal",
-]
-
-[[package]]
-name = "uuid-macro-internal"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
@@ -2087,9 +2139,9 @@ dependencies = [
[[package]]
name = "webtransport-quinn"
-version = "0.5.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b558ddb09b77347cca94bf2fd726d72c3753b60875eb3d2b7388adc12b9b4a1f"
+checksum = "4ea8dec60bceb5523139e095ff3ac4622ef0cffdd53f59fb68dd94f93f041ae4"
dependencies = [
"async-std",
"bytes",
@@ -2134,21 +2186,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-[[package]]
-name = "windows-sys"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
-dependencies = [
- "windows_aarch64_gnullvm 0.42.2",
- "windows_aarch64_msvc 0.42.2",
- "windows_i686_gnu 0.42.2",
- "windows_i686_msvc 0.42.2",
- "windows_x86_64_gnu 0.42.2",
- "windows_x86_64_gnullvm 0.42.2",
- "windows_x86_64_msvc 0.42.2",
-]
-
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -2160,99 +2197,57 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
- "windows_aarch64_gnullvm 0.48.0",
- "windows_aarch64_msvc 0.48.0",
- "windows_i686_gnu 0.48.0",
- "windows_i686_msvc 0.48.0",
- "windows_x86_64_gnu 0.48.0",
- "windows_x86_64_gnullvm 0.48.0",
- "windows_x86_64_msvc 0.48.0",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/Cargo.toml b/Cargo.toml
index 1dda68f..142b191 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,3 @@
[workspace]
-members = [
- "moq-transport",
- "moq-quinn",
- "moq-pub",
- "moq-warp",
-]
+members = ["moq-transport", "moq-relay", "moq-pub"]
+resolver = "2"
diff --git a/Dockerfile b/Dockerfile
index 733f7d6..45f7fb4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,44 +1,19 @@
FROM rust:latest as builder
-# Make a fake Rust app to keep a cached layer of compiled crates
-RUN USER=root cargo new app
-WORKDIR /usr/src/app
-COPY Cargo.toml Cargo.lock ./
-
-RUN mkdir -p moq-transport/src moq-quinn/src moq-warp/src moq-pub/src
-COPY moq-transport/Cargo.toml moq-transport/Cargo.toml
-COPY moq-quinn/Cargo.toml moq-quinn/Cargo.toml
-COPY moq-pub/Cargo.toml moq-pub/Cargo.toml
-COPY moq-warp/Cargo.toml moq-warp/Cargo.toml
-RUN touch moq-transport/src/lib.rs
-RUN touch moq-warp/src/lib.rs
-RUN touch moq-pub/src/lib.rs
-RUN touch moq-quinn/src/lib.rs
-
-RUN sed -i '/default-run.*/d' moq-quinn/Cargo.toml
-
-# Will build all dependent crates in release mode
-RUN --mount=type=cache,target=/usr/local/cargo/registry \
- --mount=type=cache,target=/usr/src/app/target \
- cargo build --release
-
-# Copy the rest
+# Create a build directory and copy over all of the files
+WORKDIR /build
COPY . .
-# Build (install) the actual binaries
-RUN cargo install --path moq-quinn
+# Reuse a cache between builds.
+# I tried to `cargo install`, but it doesn't seem to work with workspaces.
+# There's also issues with the cache mount since it builds into /usr/local/cargo/bin, and we can't mount that without clobbering cargo itself.
+# We instead we build the binaries and copy them to the cargo bin directory.
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/build/target \
+ cargo build --release && cp /build/target/release/moq-* /usr/local/cargo/bin
# Runtime image
FROM rust:latest
-# Run as "app" user
-RUN useradd -ms /bin/bash app
-
-USER app
-WORKDIR /app
-
-# Get compiled binaries from builder's cargo install directory
-COPY --from=builder /usr/local/cargo/bin/moq-quinn /app/moq-quinn
-
-ADD entrypoint.sh .
-# No CMD or ENTRYPOINT, see fly.toml with `cmd` override.
+# Copy the compiled binaries
+COPY --from=builder /usr/local/cargo/bin /usr/local/cargo/bin
diff --git a/README.md b/README.md
index 62019f1..6f817e7 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# Media over QUIC
+
+
+
+
Media over QUIC (MoQ) is a live media delivery protocol utilizing QUIC streams.
See the [MoQ working group](https://datatracker.ietf.org/wg/moq/about/) for more information.
@@ -8,7 +12,6 @@ It requires a client to actually publish/view content, such as [moq-js](https://
Join the [Discord](https://discord.gg/FCYF3p99mr) for updates and discussion.
-
## Setup
### Certificates
@@ -19,8 +22,8 @@ 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.
-```
-./cert/generate
+```bash
+./dev/cert
```
Unfortunately, WebTransport in Chrome currently (May 2023) doesn't verify certificates using the root CA.
@@ -28,16 +31,68 @@ The workaround is to use the `serverFingerprints` options, which requires the ce
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
-Run the server:
+### moq-relay
-```
-cargo run
+**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.
+
+You can run the development server with the following command, automatically using the self-signed certificate generated earlier:
+
+```bash
+./dev/relay
```
-This listens for WebTransport connections on `https://localhost:4443` by default.
-Use a [MoQ client](https://github.com/kixelated/moq-js) to connect to the server.
+Notable arguments:
+
+- `--bind ` Listen on this address [default: [::]:4443]
+- `--cert ` Use the certificate file at this path
+- `--key ` Use the private key at this path
+
+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.
+
+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
+
+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.
+
+The following command runs a development instance, broadcasing `dev/source.mp4` to `localhost:4443`:
+
+```bash
+./dev/pub
+```
+
+Notable arguments:
+
+- `` connect to the given address, which must start with moq://.
+
+### moq-js
+
+There's currently no way to consume broadcasts with `moq-rs`, at least until somebody writes `moq-sub`.
+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/).
+There's a secret `?server` parameter that can be used to connect to a different address.
+
+- Publish to localhost: `https://quic.video/publish/?server=localhost:4443`
+- Watch from localhost: `https://quic.video/watch//?server=localhost:4443`
+
+Note that self-signed certificates are ONLY supported if the server name starts with `localhost`.
+You'll need to add an entry to `/etc/hosts` if you want to use a self-signed certs and an IP address.
## License
diff --git a/cert/.gitignore b/dev/.gitignore
similarity index 50%
rename from cert/.gitignore
rename to dev/.gitignore
index 9879661..773d450 100644
--- a/cert/.gitignore
+++ b/dev/.gitignore
@@ -1,3 +1,4 @@
*.crt
*.key
-*.hex
\ No newline at end of file
+*.hex
+*.mp4
diff --git a/cert/generate b/dev/cert
similarity index 100%
rename from cert/generate
rename to dev/cert
diff --git a/cert/go.mod b/dev/go.mod
similarity index 100%
rename from cert/go.mod
rename to dev/go.mod
diff --git a/cert/go.sum b/dev/go.sum
similarity index 100%
rename from cert/go.sum
rename to dev/go.sum
diff --git a/dev/pub b/dev/pub
new file mode 100755
index 0000000..8bc94c4
--- /dev/null
+++ b/dev/pub
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -euo pipefail
+
+# Change directory to the root of the project
+cd "$(dirname "$0")/.."
+
+# Connect to localhost by default.
+HOST="${HOST:-localhost:4443}"
+
+# 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)}"
+
+# Combine the host and name into a URI.
+URI="${URI:-"moq://$HOST/$NAME"}"
+
+# Default to a source video
+MEDIA="${MEDIA:-dev/source.mp4}"
+
+# Run ffmpeg and pipe the output to moq-pub
+ffmpeg -hide_banner -v quiet \
+ -stream_loop -1 -re \
+ -i "$MEDIA" \
+ -an \
+ -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - \
+ | RUST_LOG=info cargo run --bin moq-pub -- "$URI" "$@"
diff --git a/dev/relay b/dev/relay
new file mode 100755
index 0000000..8646e3c
--- /dev/null
+++ b/dev/relay
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -euo pipefail
+
+# Change directory to the root of the project
+cd "$(dirname "$0")/.."
+
+# Default to a self-signed certificate
+# TODO automatically generate if it doesn't exist.
+CERT="${CERT:-dev/localhost.crt}"
+KEY="${KEY:-dev/localhost.key}"
+
+# Run the relay and forward any arguments
+RUST_LOG=info cargo run --bin moq-relay -- --cert "$CERT" --key "$KEY" --fingerprint "$@"
diff --git a/entrypoint.sh b/entrypoint.sh
deleted file mode 100755
index 6562f70..0000000
--- a/entrypoint.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env sh
-
-mkdir cert
-# Nothing to see here...
-echo "$MOQ_CRT" | base64 -d > cert/moq-demo.crt
-echo "$MOQ_KEY" | base64 -d > cert/moq-demo.key
-
-RUST_LOG=info ./moq-quinn --cert cert/moq-demo.crt --key cert/moq-demo.key
diff --git a/moq-pub/Cargo.toml b/moq-pub/Cargo.toml
index cf0b343..e5aa7d2 100644
--- a/moq-pub/Cargo.toml
+++ b/moq-pub/Cargo.toml
@@ -15,8 +15,6 @@ categories = ["multimedia", "network-programming", "web-programming"]
[dependencies]
moq-transport = { path = "../moq-transport" }
-#moq-transport-quinn = { path = "../moq-transport-quinn" }
-moq-warp = { path = "../moq-warp" }
# QUIC
quinn = "0.10"
@@ -36,9 +34,9 @@ tokio = { version = "1.27", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }
log = { version = "0.4", features = ["std"] }
env_logger = "0.9.3"
-anyhow = { version = "1.0.70", features = ["backtrace"]}
mp4 = "0.13.0"
rustls-native-certs = "0.6.3"
+anyhow = { version = "1.0.70", features = ["backtrace"] }
serde_json = "1.0.105"
rfc6381-codec = "0.1.0"
@@ -46,11 +44,3 @@ rfc6381-codec = "0.1.0"
http = "0.2.9"
clap = { version = "4.0", features = ["derive"] }
clap_mangen = "0.2.12"
-
-[dependencies.uuid]
-version = "1.4.1"
-features = [
- "v4", # Lets you generate random UUIDs
- "fast-rng", # Use a faster (but still sufficiently random) RNG
- "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
-]
diff --git a/moq-pub/README.md b/moq-pub/README.md
index 7b32847..7cf92a2 100644
--- a/moq-pub/README.md
+++ b/moq-pub/README.md
@@ -5,22 +5,9 @@ A command line tool for publishing media via Media over QUIC (MoQ).
Expects to receive fragmented MP4 via standard input and connect to a MOQT relay.
```
-ffmpeg ... - | moq-pub -i - -u https://localhost:4443
+ffmpeg ... - | moq-pub -i - --host localhost:4443
```
-### A note on the `moq-pub` code organization
-
-- `Media` is responsible for reading from stdin and parsing MP4 boxes. It populates a `MapSource` of `Track`s for which it holds the producer side, pushing segments of video/audio into them and notifying consumers via tokio watch async primitives.
-
-- `SessionRunner` is where we create and hold the MOQT Session from the `moq_transport` library. We currently hard-code our implementation to use `quinn` as the underlying WebTranport implementation. We use a series of `mpsc` and `broadcast` channels to make it possible for other parts of our code to send/recieve control messages via that Session. Sending Objects is handled a little differently because we are able to clone the MOQT Session's sender wherever we need to do that.
-
-- `MediaRunner` is responsible for consuming the `Track`s that `Media` produces and populates. `MediaRunner` spawns tasks for each `Track` to `.await` new segments and then put the media data into Objects and onto the wire (via channels into `SessionRunner`). Note that these tasks are created, but block waiting un the reception of a MOQT SUBSCRIBE message before they actually send any segments on the wire. `MediaRunner` is also responsible for sending the initial MOQT ANNOUNCE message announcing the namespace for the tracks we will send.
-
-- `LogViewer` as the name implies is responsible for logging. It snoops on some channels going in/out of `SessionRunner` and logs MOQT control messages.
-
-Longer term, I think it'd be interesting to refactor everything such that the `Media` + `MediaRunner` bits consume an interface that's _closer_ to what we'd like to eventually expose as a C FFI for consumption by external tools. That probably means greatly reducing the use of async Rust in the parts of this code that make up both sides of that interface boundary.
-
-
### Invoking `moq-pub`:
Here's how I'm currently testing things, with a local copy of Big Buck Bunny named `bbb_source.mp4`:
@@ -29,13 +16,13 @@ Here's how I'm currently testing things, with a local copy of Big Buck Bunny nam
$ ffmpeg -hide_banner -v quiet -stream_loop -1 -re -i bbb_source.mp4 -an -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - | RUST_LOG=moq_pub=info moq-pub -i -
```
-This relies on having `moq-quinn` (the relay server) already running locally in another shell.
+This relies on having `moq-relay` (the relay server) already running locally in another shell.
Note also that we're dropping the audio track (`-an`) above until audio playback is stabilized on the `moq-js` side.
### Known issues
-- Expects only one H.264/AVC1-encoded video track (catalog generation doesn't support audio tracks yet)
-- Doesn't yet gracefully handle EOF - workaround: never stop sending it media (`-stream_loop -1`)
-- Probably still full of lots of bugs
-- Various other TODOs you can find in the code
+- Expects only one H.264/AVC1-encoded video track (catalog generation doesn't support audio tracks yet)
+- Doesn't yet gracefully handle EOF - workaround: never stop sending it media (`-stream_loop -1`)
+- Probably still full of lots of bugs
+- Various other TODOs you can find in the code
diff --git a/moq-pub/src/cli.rs b/moq-pub/src/cli.rs
index 1e714c0..824f0dd 100644
--- a/moq-pub/src/cli.rs
+++ b/moq-pub/src/cli.rs
@@ -1,36 +1,34 @@
-use clap::{Parser, ValueEnum};
+use clap::Parser;
use std::net;
-#[derive(Parser, Clone)]
-#[command(arg_required_else_help(true))]
+#[derive(Parser, Clone, Debug)]
pub struct Config {
- #[arg(long, hide_short_help = true, default_value = "[::]:0")]
- pub bind_address: net::SocketAddr,
+ /// Listen for UDP packets on the given address.
+ #[arg(long, default_value = "[::]:0")]
+ pub bind: net::SocketAddr,
- #[arg(short, long, default_value = "https://localhost:4443")]
- pub uri: http::uri::Uri,
+ /// Advertise this frame rate in the catalog (informational)
+ // TODO auto-detect this from the input when not provided
+ #[arg(long, default_value = "24")]
+ pub fps: u8,
- #[arg(short, long, required = true, value_parser=input_parser)]
- input: InputValues,
+ /// Advertise this bit rate in the catalog (informational)
+ // TODO auto-detect this from the input when not provided
+ #[arg(long, default_value = "1500000")]
+ pub bitrate: u32,
- #[arg(long, hide_short_help = true, default_value = "24")]
- pub catalog_fps: u8,
-
- #[arg(long, hide_short_help = true, default_value = "1500000")]
- pub catalog_bit_rate: u32,
-
- #[arg(short, long, required = false, default_value = "")]
- pub namespace: String,
+ /// Connect to the given URI starting with moq://
+ #[arg(value_parser = moq_uri)]
+ pub uri: http::Uri,
}
-fn input_parser(s: &str) -> Result {
- if s == "-" {
- return Ok(InputValues::Stdin);
+fn moq_uri(s: &str) -> Result {
+ let uri = http::Uri::try_from(s).map_err(|e| e.to_string())?;
+
+ // Make sure the scheme is moq
+ if uri.scheme_str() != Some("moq") {
+ return Err("uri scheme must be moq".to_string());
}
- Err("The only currently supported input value is: '-' (stdin)".to_string())
-}
-#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
-pub enum InputValues {
- Stdin,
+ Ok(uri)
}
diff --git a/moq-pub/src/log_viewer.rs b/moq-pub/src/log_viewer.rs
deleted file mode 100644
index 92de7ea..0000000
--- a/moq-pub/src/log_viewer.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use log::{debug, info};
-use tokio::{select, sync::broadcast};
-
-pub struct LogViewer {
- incoming_ctl_receiver: broadcast::Receiver,
- incoming_obj_receiver: broadcast::Receiver,
-}
-
-impl LogViewer {
- pub async fn new(
- incoming: (
- broadcast::Receiver,
- broadcast::Receiver,
- ),
- ) -> anyhow::Result {
- Ok(Self {
- incoming_ctl_receiver: incoming.0,
- incoming_obj_receiver: incoming.1,
- })
- }
- pub async fn run(&mut self) -> anyhow::Result<()> {
- debug!("log_viewer.run()");
-
- loop {
- select! {
- msg = self.incoming_ctl_receiver.recv() => {
- info!(
- "Received incoming MOQT Control message: {:?}",
- &msg?
- );}
- obj = self.incoming_obj_receiver.recv() => {
- info!(
- "Received incoming MOQT Object with header: {:?}",
- &obj?
- );}
- }
- }
- }
-}
diff --git a/moq-pub/src/main.rs b/moq-pub/src/main.rs
index 92ed357..0f01e32 100644
--- a/moq-pub/src/main.rs
+++ b/moq-pub/src/main.rs
@@ -1,23 +1,13 @@
use anyhow::Context;
use clap::Parser;
-use tokio::task::JoinSet;
-
-mod session_runner;
-use session_runner::*;
-
-mod media_runner;
-use media_runner::*;
-
-mod log_viewer;
-use log_viewer::*;
-
-mod media;
-use media::*;
mod cli;
use cli::*;
-use uuid::Uuid;
+mod media;
+use media::*;
+
+use moq_transport::model::broadcast;
// TODO: clap complete
@@ -25,35 +15,49 @@ use uuid::Uuid;
async fn main() -> anyhow::Result<()> {
env_logger::init();
- let mut config = Config::parse();
+ let config = Config::parse();
- if config.namespace.is_empty() {
- config.namespace = format!("quic.video/{}", Uuid::new_v4());
+ let (publisher, subscriber) = broadcast::new();
+ let mut media = Media::new(&config, publisher).await?;
+
+ // Ugh, just let me use my native root certs already
+ let mut roots = rustls::RootCertStore::empty();
+ for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
+ roots.add(&rustls::Certificate(cert.0)).unwrap();
}
- let mut media = Media::new(&config).await?;
- let session_runner = SessionRunner::new(&config).await?;
- let mut log_viewer = LogViewer::new(session_runner.get_incoming_receivers().await).await?;
- let mut media_runner = MediaRunner::new(
- session_runner.get_send_objects().await,
- session_runner.get_outgoing_senders().await,
- session_runner.get_incoming_receivers().await,
- )
- .await?;
+ let mut tls_config = rustls::ClientConfig::builder()
+ .with_safe_defaults()
+ .with_root_certificates(roots)
+ .with_no_client_auth();
- let mut join_set: JoinSet> = tokio::task::JoinSet::new();
+ tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; // this one is important
- join_set.spawn(async { session_runner.run().await.context("failed to run session runner") });
- join_set.spawn(async move { log_viewer.run().await.context("failed to run media source") });
+ let arc_tls_config = std::sync::Arc::new(tls_config);
+ let quinn_client_config = quinn::ClientConfig::new(arc_tls_config);
- media_runner.announce(&config.namespace, media.source()).await?;
+ let mut endpoint = quinn::Endpoint::client(config.bind)?;
+ endpoint.set_default_client_config(quinn_client_config);
- join_set.spawn(async move { media.run().await.context("failed to run media source") });
- join_set.spawn(async move { media_runner.run().await.context("failed to run client") });
+ log::info!("connecting to {}", config.uri);
- while let Some(res) = join_set.join_next().await {
- dbg!(&res);
- res??;
+ // Change the uri scheme to "https" for WebTransport
+ 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
+ .context("failed to create WebTransport session")?;
+
+ let session = moq_transport::session::Client::publisher(session, subscriber)
+ .await
+ .context("failed to create MoQ Transport session")?;
+
+ // TODO run a task that returns a 404 for all unknown subscriptions.
+ tokio::select! {
+ res = session.run() => res.context("session error")?,
+ res = media.run() => res.context("media error")?,
}
Ok(())
diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs
index 650d411..2fe1205 100644
--- a/moq-pub/src/media.rs
+++ b/moq-pub/src/media.rs
@@ -1,25 +1,25 @@
use crate::cli::Config;
use anyhow::{self, Context};
-use log::{debug, info};
+use moq_transport::model::{broadcast, segment, track};
use moq_transport::VarInt;
-use moq_warp::model::{segment, track};
use mp4::{self, ReadBox};
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
-use std::sync::Arc;
use std::time;
use tokio::io::AsyncReadExt;
pub struct Media {
- // The tracks we're producing.
- tracks: HashMap,
+ // We hold on to publisher so we don't close then while media is still being published.
+ _broadcast: broadcast::Publisher,
+ _catalog: track::Publisher,
+ _init: track::Publisher,
- source: Arc,
+ tracks: HashMap,
}
impl Media {
- pub async fn new(config: &Config) -> anyhow::Result {
+ pub async fn new(config: &Config, mut broadcast: broadcast::Publisher) -> anyhow::Result {
let mut stdin = tokio::io::stdin();
let ftyp = read_atom(&mut stdin).await?;
anyhow::ensure!(&ftyp[4..8] == b"ftyp", "expected ftyp atom");
@@ -38,45 +38,43 @@ impl Media {
// Parse the moov box so we can detect the timescales for each track.
let moov = mp4::MoovBox::read_box(&mut moov_reader, moov_header.size)?;
- // Create a source that can be subscribed to.
- let mut source = HashMap::default();
+ // Create the catalog track with a single segment.
+ let mut init_track = broadcast.create_track("1.mp4")?;
+ let mut init_segment = init_track.create_segment(segment::Info {
+ sequence: VarInt::ZERO,
+ priority: i32::MAX,
+ expires: None,
+ })?;
+
+ init_segment.write_chunk(init.into())?;
let mut tracks = HashMap::new();
- // Create the init track
- let init_track_name = "1.mp4";
- let (_init, subscriber) = Self::create_init(init);
- source.insert(init_track_name.to_string(), subscriber);
-
for trak in &moov.traks {
let id = trak.tkhd.track_id;
let name = id.to_string();
- //let name = "2".to_string();
- //dbg!("trak name: {}", &name);
let timescale = track_timescale(&moov, id);
// Store the track publisher in a map so we can update it later.
- let track = Track::new(&name, timescale);
- source.insert(name.to_string(), track.subscribe());
-
+ let track = broadcast.create_track(&name)?;
+ let track = Track::new(track, timescale);
tracks.insert(name, track);
}
+ let mut catalog = broadcast.create_track(".catalog")?;
+
// Create the catalog track
- let (_catalog, subscriber) = Self::create_catalog(
- config,
- config.namespace.to_string(),
- init_track_name.to_string(),
- &moov,
- &tracks,
- )?;
- source.insert(".catalog".to_string(), subscriber);
+ Self::serve_catalog(&mut catalog, config, init_track.name.to_string(), &moov, &tracks)?;
- let source = Arc::new(MapSource(source));
-
- Ok(Media { tracks, source })
+ Ok(Media {
+ _broadcast: broadcast,
+ _catalog: catalog,
+ _init: init_track,
+ tracks,
+ })
}
+
pub async fn run(&mut self) -> anyhow::Result<()> {
let mut stdin = tokio::io::stdin();
// The current track name
@@ -122,45 +120,18 @@ impl Media {
}
}
- fn create_init(raw: Vec) -> (track::Publisher, track::Subscriber) {
- // Create a track with a single segment containing the init data.
- let mut init_track = track::Publisher::new("1.mp4");
-
- // Subscribe to the init track before we push the segment.
- let subscriber = init_track.subscribe();
-
- let mut segment = segment::Publisher::new(segment::Info {
- sequence: VarInt::from_u32(0), // first and only segment
- send_order: i32::MIN, // highest priority
- expires: None, // never delete from the cache
- });
-
- // Add the segment and add the fragment.
- init_track.push_segment(segment.subscribe());
- segment.fragments.push(raw.into());
-
- // Return the catalog
- (init_track, subscriber)
- }
-
- fn create_catalog(
+ fn serve_catalog(
+ track: &mut track::Publisher,
config: &Config,
- namespace: String,
init_track_name: String,
moov: &mp4::MoovBox,
_tracks: &HashMap,
- ) -> Result<(track::Publisher, track::Subscriber), anyhow::Error> {
- // Create a track with a single segment containing the init data.
- let mut catalog_track = track::Publisher::new(".catalog");
-
- // Subscribe to the catalog before we push the segment.
- let catalog_subscriber = catalog_track.subscribe();
-
- let mut segment = segment::Publisher::new(segment::Info {
- sequence: VarInt::from_u32(0), // first and only segment
- send_order: i32::MIN, // highest priority
- expires: None, // never delete from the cache
- });
+ ) -> Result<(), anyhow::Error> {
+ let mut segment = track.create_segment(segment::Info {
+ sequence: VarInt::ZERO,
+ priority: i32::MAX,
+ expires: None,
+ })?;
// avc1[.PPCCLL]
//
@@ -192,30 +163,24 @@ impl Media {
"tracks": [
{
"container": "mp4",
- "namespace": namespace,
"kind": "video",
"init_track": init_track_name,
"data_track": "1", // assume just one track for now
"codec": codec_str,
"width": width,
"height": height,
- "frame_rate": config.catalog_fps,
- "bit_rate": config.catalog_bit_rate,
+ "frame_rate": config.fps,
+ "bit_rate": config.bitrate,
}
]
});
let catalog_str = serde_json::to_string_pretty(&catalog)?;
- info!("catalog: {}", catalog_str);
+ log::info!("catalog: {}", catalog_str);
// Add the segment and add the fragment.
- catalog_track.push_segment(segment.subscribe());
- segment.fragments.push(catalog_str.into());
+ segment.write_chunk(catalog_str.into())?;
- // Return the catalog
- Ok((catalog_track, catalog_subscriber))
- }
- pub fn source(&self) -> Arc {
- self.source.clone()
+ Ok(())
}
}
@@ -230,8 +195,6 @@ async fn read_atom(reader: &mut R) -> anyhow::Result reader.take(u64::MAX),
@@ -249,13 +212,11 @@ async fn read_atom(reader: &mut R) -> anyhow::Result reader.take(size - 8),
};
// Append to the vector and return it.
- let read_bytes = limit.read_to_end(&mut raw).await?;
- debug!("read_bytes: {}", read_bytes);
+ let _read_bytes = limit.read_to_end(&mut raw).await?;
Ok(raw)
}
@@ -275,9 +236,7 @@ struct Track {
}
impl Track {
- fn new(name: &str, timescale: u64) -> Self {
- let track = track::Publisher::new(name);
-
+ fn new(track: track::Publisher, timescale: u64) -> Self {
Self {
track,
sequence: 0,
@@ -290,13 +249,12 @@ impl Track {
if let Some(segment) = self.segment.as_mut() {
if !fragment.keyframe {
// Use the existing segment
- segment.fragments.push(raw.into());
+ segment.write_chunk(raw.into())?;
return Ok(());
}
}
// Otherwise make a new segment
- let now = time::Instant::now();
// Compute the timestamp in milliseconds.
// Overflows after 583 million years, so we're fine.
@@ -306,50 +264,32 @@ impl Track {
.try_into()
.context("timestamp too large")?;
- // The send order is simple; newer timestamps should be higher priority.
- // TODO give audio a boost?
- // TODO Use timestamps for prioritization again after quinn priority bug fixed
- let send_order = i32::MIN;
+ // Create a new segment.
+ let mut segment = self.track.create_segment(segment::Info {
+ sequence: VarInt::try_from(self.sequence).context("sequence too large")?,
+ priority: i32::MAX, // TODO
- // Delete segments after 10s.
- let expires = Some(now + time::Duration::from_secs(10)); // TODO increase this once send order is implemented
- let sequence = self.sequence.try_into().context("sequence too large")?;
+ // Delete segments after 10s.
+ expires: Some(time::Duration::from_secs(10)),
+ })?;
self.sequence += 1;
- // Create a new segment.
- let segment = segment::Info {
- sequence,
- expires,
- send_order,
- };
-
- let mut segment = segment::Publisher::new(segment);
- self.track.push_segment(segment.subscribe());
-
// Insert the raw atom into the segment.
- segment.fragments.push(raw.into());
+ segment.write_chunk(raw.into())?;
// Save for the next iteration
self.segment = Some(segment);
- // Remove any segments older than 10s.
- // TODO This can only drain from the FRONT of the queue, so don't get clever with expirations.
- self.track.drain_segments(now);
-
Ok(())
}
pub fn data(&mut self, raw: Vec) -> anyhow::Result<()> {
let segment = self.segment.as_mut().context("missing segment")?;
- segment.fragments.push(raw.into());
+ segment.write_chunk(raw.into())?;
Ok(())
}
-
- pub fn subscribe(&self) -> track::Subscriber {
- self.track.subscribe()
- }
}
struct Fragment {
@@ -434,16 +374,3 @@ fn track_timescale(moov: &mp4::MoovBox, track_id: u32) -> u64 {
trak.mdia.mdhd.timescale as u64
}
-
-pub trait Source {
- fn subscribe(&self, name: &str) -> Option;
-}
-
-#[derive(Clone, Default, Debug)]
-pub struct MapSource(pub HashMap);
-
-impl Source for MapSource {
- fn subscribe(&self, name: &str) -> Option {
- self.0.get(name).cloned()
- }
-}
diff --git a/moq-pub/src/media_runner.rs b/moq-pub/src/media_runner.rs
deleted file mode 100644
index fa1a02c..0000000
--- a/moq-pub/src/media_runner.rs
+++ /dev/null
@@ -1,159 +0,0 @@
-use crate::media::{self, MapSource};
-use anyhow::bail;
-use log::{debug, error};
-use moq_transport::message::Message;
-use moq_transport::message::{Announce, SubscribeError, SubscribeOk};
-use moq_transport::{object, Object, VarInt};
-use std::collections::HashMap;
-use std::sync::Arc;
-use tokio::io::AsyncWriteExt;
-use tokio::sync::broadcast;
-use tokio::sync::mpsc;
-use tokio::task::JoinSet;
-
-use webtransport_generic::Session as WTSession;
-
-pub struct MediaRunner {
- send_objects: object::Sender,
- outgoing_ctl_sender: mpsc::Sender,
- incoming_ctl_receiver: broadcast::Receiver,
- source: Arc,
-}
-
-impl MediaRunner {
- pub async fn new(
- send_objects: object::Sender,
- outgoing: mpsc::Sender,
- incoming: (broadcast::Receiver, broadcast::Receiver),
- ) -> anyhow::Result {
- let outgoing_ctl_sender = outgoing;
- let (incoming_ctl_receiver, _incoming_obj_receiver) = incoming;
- Ok(Self {
- send_objects,
- outgoing_ctl_sender,
- incoming_ctl_receiver,
- source: Arc::new(MapSource::default()),
- })
- }
- pub async fn announce(&mut self, namespace: &str, source: Arc) -> anyhow::Result<()> {
- debug!("media_runner.announce()");
- // Only allow one souce at a time for now?
- self.source = source;
-
- // ANNOUNCE the namespace
- self.outgoing_ctl_sender
- .send(Message::Announce(Announce {
- track_namespace: namespace.to_string(),
- }))
- .await?;
-
- // wait for the go ahead
- loop {
- match self.incoming_ctl_receiver.recv().await? {
- Message::AnnounceOk(_) => {
- break;
- }
- Message::AnnounceError(announce_error) => {
- error!(
- "Failed to announce namespace '{}' with error code '{}' and reason '{}'",
- &namespace, &announce_error.code, &announce_error.reason
- );
- // TODO: Think about how to recover here? Retry?
- bail!("Failed to announce namespace");
- }
- _ => {
- // TODO: work out how to ignore unknown/unrelated messages here without consuming them prematurely
- }
- }
- }
-
- Ok(())
- }
-
- pub async fn run(&mut self) -> anyhow::Result<()> {
- debug!("media_runner.run()");
- let source = self.source.clone();
- let mut join_set: JoinSet> = tokio::task::JoinSet::new();
- let mut track_dispatcher: HashMap> = HashMap::new();
- let mut incoming_ctl_receiver = self.incoming_ctl_receiver.resubscribe();
- let outgoing_ctl_sender = self.outgoing_ctl_sender.clone();
-
- // Pre-spawn tasks for each track we have
- // and let them .await on receiving the go ahead via a channel
- for (track_name, track) in source.0.iter() {
- let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
- track_dispatcher.insert(track_name.to_string(), sender);
- let mut objects = self.send_objects.clone();
- let mut track = track.clone();
- join_set.spawn(async move {
- let track_id = receiver.recv().await.ok_or(anyhow::anyhow!("channel closed"))?;
- // TODO: validate track_id is valid (not already in use), for now just trust subscribers are correct
- loop {
- let mut segment = track.next_segment().await?;
-
- debug!("segment: {:?}", &segment);
- let object = Object {
- track: track_id,
- group: segment.sequence,
- sequence: VarInt::from_u32(0), // Always zero since we send an entire group as an object
- send_order: segment.send_order,
- };
- debug!("object: {:?}", &object);
-
- let mut stream = objects.open(object).await?;
-
- // Write each fragment as they are available.
- while let Some(fragment) = segment.fragments.next().await {
- stream.write_all(&fragment).await?;
- }
- }
- });
- }
-
- join_set.spawn(async move {
- loop {
- if let Message::Subscribe(subscribe) = incoming_ctl_receiver.recv().await? {
- debug!("Received a subscription request");
-
- let track_id = subscribe.track_id;
- let track_name = subscribe.track_name;
- debug!("Looking up track_name: {} (track_id: {})", &track_name, &track_id);
- // Look up track in source
- match source.0.get(&track_name.to_string()) {
- None => {
- // if track !exist, send subscribe error
- outgoing_ctl_sender
- .send(Message::SubscribeError(SubscribeError {
- track_id: subscribe.track_id,
- code: moq_transport::VarInt::from_u32(1),
- reason: "Only bad reasons (don't know what that track is)".to_string(),
- }))
- .await?;
- }
- // if track exists, send go-ahead signal to unblock task to send data to subscriber
- Some(track) => {
- debug!("We have the track! (Good news everyone)");
- track_dispatcher
- .get(&track.name)
- .ok_or(anyhow::anyhow!("missing task for track"))?
- .send(track_id)
- .await?;
- outgoing_ctl_sender
- .send(Message::SubscribeOk(SubscribeOk {
- track_id: subscribe.track_id,
- expires: Some(VarInt::from_u32(0)), // valid until unsubscribed
- }))
- .await?;
- }
- };
- }
- }
- });
-
- while let Some(res) = join_set.join_next().await {
- debug!("MediaRunner task finished with result: {:?}", &res);
- }
-
- Ok(())
- }
-}
diff --git a/moq-pub/src/session_runner.rs b/moq-pub/src/session_runner.rs
deleted file mode 100644
index 4ee38cf..0000000
--- a/moq-pub/src/session_runner.rs
+++ /dev/null
@@ -1,122 +0,0 @@
-use crate::cli::Config;
-use anyhow::Context;
-use log::debug;
-use moq_transport::{object, Object};
-use tokio::sync::broadcast;
-use tokio::sync::mpsc;
-use tokio::task::JoinSet;
-
-pub struct SessionRunner {
- moq_transport_session: moq_transport::Session,
- outgoing_ctl_sender: mpsc::Sender,
- outgoing_ctl_receiver: mpsc::Receiver,
- incoming_ctl_sender: broadcast::Sender,
- incoming_obj_sender: broadcast::Sender,
-}
-
-impl SessionRunner {
- pub async fn new(config: &Config) -> anyhow::Result {
- let mut roots = rustls::RootCertStore::empty();
- for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
- roots.add(&rustls::Certificate(cert.0)).unwrap();
- }
-
- let mut tls_config = rustls::ClientConfig::builder()
- .with_safe_defaults()
- .with_root_certificates(roots)
- .with_no_client_auth();
-
- tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; // this one is important
-
- let arc_tls_config = std::sync::Arc::new(tls_config);
- let quinn_client_config = quinn::ClientConfig::new(arc_tls_config);
-
- let mut endpoint = quinn::Endpoint::client(config.bind_address)?;
- endpoint.set_default_client_config(quinn_client_config);
-
- let webtransport_session = webtransport_quinn::connect(&endpoint, &config.uri)
- .await
- .context("failed to create WebTransport session")?;
- let moq_transport_session =
- moq_transport::Session::connect(webtransport_session, moq_transport::setup::Role::Both)
- .await
- .context("failed to create MoQ Transport session")?;
-
- // outgoing ctl msgs
- let (outgoing_ctl_sender, outgoing_ctl_receiver) = mpsc::channel(5);
- // incoming ctl msg
- let (incoming_ctl_sender, _incoming_ctl_receiver) = broadcast::channel(5);
- // incoming objs
- let (incoming_obj_sender, _incoming_obj_receiver) = broadcast::channel(5);
-
- Ok(SessionRunner {
- moq_transport_session,
- outgoing_ctl_sender,
- outgoing_ctl_receiver,
- incoming_ctl_sender,
- incoming_obj_sender,
- })
- }
- pub async fn get_outgoing_senders(&self) -> mpsc::Sender {
- self.outgoing_ctl_sender.clone()
- }
- pub async fn get_incoming_receivers(
- &self,
- ) -> (
- broadcast::Receiver,
- broadcast::Receiver,
- ) {
- (
- self.incoming_ctl_sender.subscribe(),
- self.incoming_obj_sender.subscribe(),
- )
- }
- pub async fn run(mut self) -> anyhow::Result<()> {
- debug!("session_runner.run()");
-
- let mut join_set: JoinSet> = tokio::task::JoinSet::new();
-
- // Send outgoing control messages
- join_set.spawn(async move {
- loop {
- let msg = self
- .outgoing_ctl_receiver
- .recv()
- .await
- .ok_or(anyhow::anyhow!("error receiving outbound control message"))?;
- debug!("Sending outgoing MOQT Control Message: {:?}", &msg);
- self.moq_transport_session.send_control.send(msg).await?;
- }
- });
-
- // Route incoming Control messages
- join_set.spawn(async move {
- loop {
- let msg = self.moq_transport_session.recv_control.recv().await?;
- self.incoming_ctl_sender.send(msg)?;
- }
- });
-
- // Route incoming Objects headers
- // NOTE: Only sends the headers for incoming objects, not the associated streams
- // We don't currently expose any way to read incoming bytestreams because we don't expect any
- join_set.spawn(async move {
- loop {
- let receive_stream = self.moq_transport_session.recv_objects.recv().await?;
-
- self.incoming_obj_sender.send(receive_stream.0)?;
- }
- });
-
- while let Some(res) = join_set.join_next().await {
- debug!("SessionRunner task finished with result: {:?}", &res);
- let _ = res?; // if we finish, it'll be with an error, which we can return
- }
-
- Ok(())
- }
-
- pub async fn get_send_objects(&self) -> object::Sender {
- self.moq_transport_session.send_objects.clone()
- }
-}
diff --git a/moq-quinn/Cargo.toml b/moq-relay/Cargo.toml
similarity index 82%
rename from moq-quinn/Cargo.toml
rename to moq-relay/Cargo.toml
index ac4b500..b6907fd 100644
--- a/moq-quinn/Cargo.toml
+++ b/moq-relay/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "moq-quinn"
+name = "moq-relay"
description = "Media over QUIC"
authors = ["Luke Curley"]
repository = "https://github.com/kixelated/moq-rs"
@@ -11,14 +11,8 @@ edition = "2021"
keywords = ["quic", "http3", "webtransport", "media", "live"]
categories = ["multimedia", "network-programming", "web-programming"]
-default-run = "moq-quinn"
-
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
[dependencies]
moq-transport = { path = "../moq-transport" }
-moq-warp = { path = "../moq-warp" }
# QUIC
quinn = "0.10"
@@ -42,3 +36,5 @@ clap = { version = "4.0", features = ["derive"] }
log = { version = "0.4", features = ["std"] }
env_logger = "0.9.3"
anyhow = "1.0.70"
+tracing = "0.1"
+tracing-subscriber = "0.3.0"
diff --git a/moq-relay/README.md b/moq-relay/README.md
new file mode 100644
index 0000000..04d4c2e
--- /dev/null
+++ b/moq-relay/README.md
@@ -0,0 +1,17 @@
+# moq-relay
+
+A server that connects publishing clients to subscribing clients.
+All subscriptions are deduplicated and cached, so that a single publisher can serve many subscribers.
+
+## Usage
+
+The publisher must choose a unique name for their broadcast, sent as the WebTransport path when connecting to the server.
+We currently do a dumb string comparison, so capatilization matters as do slashes.
+
+For example: `CONNECT https://relay.quic.video/BigBuckBunny`
+
+The MoqTransport handshake includes a `role` parameter, which must be `publisher` or `subscriber`.
+The specification allows a `both` role but you'll get an error.
+
+You can have one publisher and any number of subscribers connected to the same path.
+If the publisher disconnects, then all subscribers receive an error and will not get updates, even if a new publisher reuses the path.
diff --git a/moq-relay/src/config.rs b/moq-relay/src/config.rs
new file mode 100644
index 0000000..070edce
--- /dev/null
+++ b/moq-relay/src/config.rs
@@ -0,0 +1,23 @@
+use std::{net, path};
+
+use clap::Parser;
+
+/// Search for a pattern in a file and display the lines that contain it.
+#[derive(Parser, Clone)]
+pub struct Config {
+ /// Listen on this address
+ #[arg(long, default_value = "[::]:4443")]
+ pub bind: net::SocketAddr,
+
+ /// Use the certificate file at this path
+ #[arg(long)]
+ pub cert: path::PathBuf,
+
+ /// Use the private key at this path
+ #[arg(long)]
+ pub key: path::PathBuf,
+
+ /// Listen on HTTPS and serve /fingerprint, for self-signed certificates
+ #[arg(long, action)]
+ pub fingerprint: bool,
+}
diff --git a/moq-quinn/src/main.rs b/moq-relay/src/main.rs
similarity index 55%
rename from moq-quinn/src/main.rs
rename to moq-relay/src/main.rs
index dfa4655..471eea7 100644
--- a/moq-quinn/src/main.rs
+++ b/moq-relay/src/main.rs
@@ -1,59 +1,45 @@
-use std::{fs, io, net, path, sync};
+use std::{fs, io, sync};
use anyhow::Context;
use clap::Parser;
use ring::digest::{digest, SHA256};
use warp::Filter;
+mod config;
mod server;
-use server::*;
+mod session;
-/// Search for a pattern in a file and display the lines that contain it.
-#[derive(Parser, Clone)]
-struct Cli {
- /// Listen on this address
- #[arg(short, long, default_value = "[::]:4443")]
- addr: net::SocketAddr,
-
- /// Use the certificate file at this path
- #[arg(short, long, default_value = "cert/localhost.crt")]
- cert: path::PathBuf,
-
- /// Use the private key at this path
- #[arg(short, long, default_value = "cert/localhost.key")]
- key: path::PathBuf,
-}
+pub use config::*;
+pub use server::*;
+pub use session::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
- let args = Cli::parse();
+ // Disable tracing so we don't get a bunch of Quinn spam.
+ let tracer = tracing_subscriber::FmtSubscriber::builder()
+ .with_max_level(tracing::Level::WARN)
+ .finish();
+ tracing::subscriber::set_global_default(tracer).unwrap();
- // Create a web server to serve the fingerprint
- let serve = serve_http(args.clone());
+ let config = Config::parse();
// Create a server to actually serve the media
- let config = ServerConfig {
- addr: args.addr,
- cert: args.cert,
- key: args.key,
- };
-
- let server = Server::new(config).context("failed to create server")?;
+ let server = Server::new(config.clone()).context("failed to create server")?;
// Run all of the above
tokio::select! {
res = server.run() => res.context("failed to run server"),
- res = serve => res.context("failed to run HTTP server"),
+ res = serve_http(config), if config.fingerprint => res.context("failed to run HTTP server"),
}
}
// Run a HTTP server using Warp
// TODO remove this when Chrome adds support for self-signed certificates using WebTransport
-async fn serve_http(args: Cli) -> anyhow::Result<()> {
+async fn serve_http(config: Config) -> anyhow::Result<()> {
// Read the PEM certificate file
- let crt = fs::File::open(&args.cert)?;
+ let crt = fs::File::open(&config.cert)?;
let mut crt = io::BufReader::new(crt);
// Parse the DER certificate
@@ -75,9 +61,9 @@ async fn serve_http(args: Cli) -> anyhow::Result<()> {
warp::serve(routes)
.tls()
- .cert_path(args.cert)
- .key_path(args.key)
- .run(args.addr)
+ .cert_path(config.cert)
+ .key_path(config.key)
+ .run(config.bind)
.await;
Ok(())
diff --git a/moq-quinn/src/server.rs b/moq-relay/src/server.rs
similarity index 51%
rename from moq-quinn/src/server.rs
rename to moq-relay/src/server.rs
index 59c227d..c45511f 100644
--- a/moq-quinn/src/server.rs
+++ b/moq-relay/src/server.rs
@@ -1,29 +1,30 @@
-use std::{fs, io, net, path, sync, time};
+use std::{
+ collections::HashMap,
+ fs, io,
+ sync::{Arc, Mutex},
+ time,
+};
use anyhow::Context;
-use moq_warp::relay;
+use moq_transport::model::broadcast;
use tokio::task::JoinSet;
+use crate::{Config, Session};
+
pub struct Server {
server: quinn::Endpoint,
- // The media sources.
- broker: relay::Broker,
-
// The active connections.
conns: JoinSet>,
-}
-pub struct ServerConfig {
- pub addr: net::SocketAddr,
- pub cert: path::PathBuf,
- pub key: path::PathBuf,
+ // The map of active broadcasts by path.
+ broadcasts: Arc>>,
}
impl Server {
// Create a new server
- pub fn new(config: ServerConfig) -> anyhow::Result {
+ pub fn new(config: Config) -> anyhow::Result {
// Read the PEM certificate chain
let certs = fs::File::open(config.cert).context("failed to open cert file")?;
let mut certs = io::BufReader::new(certs);
@@ -51,21 +52,25 @@ impl Server {
tls_config.max_early_data_size = u32::MAX;
tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()];
- let mut server_config = quinn::ServerConfig::with_crypto(sync::Arc::new(tls_config));
+ let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(tls_config));
// Enable BBR congestion control
// TODO validate the implementation
let mut transport_config = quinn::TransportConfig::default();
transport_config.keep_alive_interval(Some(time::Duration::from_secs(2)));
- transport_config.congestion_controller_factory(sync::Arc::new(quinn::congestion::BbrConfig::default()));
+ transport_config.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default()));
- server_config.transport = sync::Arc::new(transport_config);
- let server = quinn::Endpoint::server(server_config, config.addr)?;
- let broker = relay::Broker::new();
+ server_config.transport = Arc::new(transport_config);
+ let server = quinn::Endpoint::server(server_config, config.bind)?;
+ let broadcasts = Default::default();
let conns = JoinSet::new();
- Ok(Self { server, broker, conns })
+ Ok(Self {
+ server,
+ broadcasts,
+ conns,
+ })
}
pub async fn run(mut self) -> anyhow::Result<()> {
@@ -73,44 +78,16 @@ impl Server {
tokio::select! {
res = self.server.accept() => {
let conn = res.context("failed to accept QUIC connection")?;
- let broker = self.broker.clone();
-
- self.conns.spawn(async move { Self::handle(conn, broker).await });
+ let mut session = Session::new(self.broadcasts.clone());
+ self.conns.spawn(async move { session.run(conn).await });
},
res = self.conns.join_next(), if !self.conns.is_empty() => {
let res = res.expect("no tasks").expect("task aborted");
if let Err(err) = res {
- log::error!("connection terminated: {:?}", err);
+ log::warn!("connection terminated: {:?}", err);
}
},
}
}
}
-
- async fn handle(conn: quinn::Connecting, broker: relay::Broker) -> anyhow::Result<()> {
- // Wait for the QUIC connection to be established.
- let conn = conn.await.context("failed to establish QUIC connection")?;
-
- // Wait for the CONNECT request.
- let request = webtransport_quinn::accept(conn)
- .await
- .context("failed to receive WebTransport request")?;
-
- // TODO parse the request URI
-
- // Accept the CONNECT request.
- let session = request
- .ok()
- .await
- .context("failed to respond to WebTransport request")?;
-
- // Perform the MoQ handshake.
- let session = moq_transport::Session::accept(session, moq_transport::setup::Role::Both)
- .await
- .context("failed to perform MoQ handshake")?;
-
- // Run the relay code.
- let session = relay::Session::new(session, broker);
- session.run().await
- }
}
diff --git a/moq-relay/src/session.rs b/moq-relay/src/session.rs
new file mode 100644
index 0000000..dcc367d
--- /dev/null
+++ b/moq-relay/src/session.rs
@@ -0,0 +1,96 @@
+use std::{
+ collections::{hash_map, HashMap},
+ sync::{Arc, Mutex},
+};
+
+use anyhow::Context;
+
+use moq_transport::{model::broadcast, session::Request, setup::Role};
+
+#[derive(Clone)]
+pub struct Session {
+ broadcasts: Arc>>,
+}
+
+impl Session {
+ pub fn new(broadcasts: Arc>>) -> Self {
+ Self { broadcasts }
+ }
+
+ pub async fn run(&mut self, conn: quinn::Connecting) -> anyhow::Result<()> {
+ // Wait for the QUIC connection to be established.
+ let conn = conn.await.context("failed to establish QUIC connection")?;
+
+ // Wait for the CONNECT request.
+ let request = webtransport_quinn::accept(conn)
+ .await
+ .context("failed to receive WebTransport request")?;
+
+ let path = request.uri().path().to_string();
+
+ // Accept the CONNECT request.
+ let session = request
+ .ok()
+ .await
+ .context("failed to respond to WebTransport request")?;
+
+ // Perform the MoQ handshake.
+ let request = moq_transport::session::Server::accept(session)
+ .await
+ .context("failed to accept handshake")?;
+
+ let role = request.role();
+
+ match role {
+ Role::Publisher => self.serve_publisher(request, &path).await,
+ Role::Subscriber => self.serve_subscriber(request, &path).await,
+ Role::Both => request.reject(300),
+ };
+
+ Ok(())
+ }
+
+ async fn serve_publisher(&mut self, request: Request, path: &str) {
+ log::info!("publisher: path={}", path);
+
+ let (publisher, subscriber) = broadcast::new();
+
+ match self.broadcasts.lock().unwrap().entry(path.to_string()) {
+ hash_map::Entry::Occupied(_) => return request.reject(409),
+ hash_map::Entry::Vacant(entry) => entry.insert(subscriber),
+ };
+
+ if let Err(err) = self.run_publisher(request, publisher).await {
+ log::warn!("pubisher error: path={} err={:?}", path, err);
+ }
+
+ self.broadcasts.lock().unwrap().remove(path);
+ }
+
+ async fn run_publisher(&mut self, request: Request, publisher: broadcast::Publisher) -> anyhow::Result<()> {
+ let session = request.subscriber(publisher).await?;
+ session.run().await?;
+ Ok(())
+ }
+
+ async fn serve_subscriber(&mut self, request: Request, path: &str) {
+ log::info!("subscriber: path={}", path);
+
+ let broadcast = match self.broadcasts.lock().unwrap().get(path) {
+ Some(broadcast) => broadcast.clone(),
+ None => {
+ return request.reject(404);
+ }
+ };
+
+ if let Err(err) = self.run_subscriber(request, broadcast).await {
+ log::warn!("subscriber error: path={} err={:?}", path, err);
+ }
+ }
+
+ async fn run_subscriber(&mut self, request: Request, broadcast: broadcast::Subscriber) -> anyhow::Result<()> {
+ let session = request.publisher(broadcast).await?;
+ session.run().await?;
+ Ok(())
+ }
+}
diff --git a/moq-transport/Cargo.toml b/moq-transport/Cargo.toml
index 5a8f3ba..b99648a 100644
--- a/moq-transport/Cargo.toml
+++ b/moq-transport/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Luke Curley"]
repository = "https://github.com/kixelated/moq-rs"
license = "MIT OR Apache-2.0"
-version = "0.1.0"
+version = "0.2.0"
edition = "2021"
keywords = ["quic", "http3", "webtransport", "media", "live"]
@@ -18,5 +18,9 @@ categories = ["multimedia", "network-programming", "web-programming"]
bytes = "1.4"
thiserror = "1"
anyhow = "1"
-webtransport-generic = "0.5"
-tokio = { version = "1.27", features = ["macros", "io-util", "rt", "sync"] }
+tokio = { version = "1.27", features = ["macros", "io-util", "sync"] }
+log = "0.4"
+indexmap = "2"
+
+quinn = "0.10"
+webtransport-quinn = "0.5.2"
diff --git a/moq-transport/README.md b/moq-transport/README.md
new file mode 100644
index 0000000..7788103
--- /dev/null
+++ b/moq-transport/README.md
@@ -0,0 +1,10 @@
+[![Documentation](https://docs.rs/moq-transport/badge.svg)](https://docs.rs/moq-transport/)
+[![Crates.io](https://img.shields.io/crates/v/moq-transport.svg)](https://crates.io/crates/moq-transport)
+[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT)
+
+# moq-transport
+
+A Rust implementation of the proposed IETF standard.
+
+[Specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/)
+[Github](https://github.com/moq-wg/moq-transport)
diff --git a/moq-transport/src/coding/decode.rs b/moq-transport/src/coding/decode.rs
index e2f8f3a..7a84a55 100644
--- a/moq-transport/src/coding/decode.rs
+++ b/moq-transport/src/coding/decode.rs
@@ -1,8 +1,14 @@
-use super::VarInt;
+use super::{BoundsExceeded, VarInt};
use std::str;
use thiserror::Error;
+// I'm too lazy to add these trait bounds to every message type.
+// TODO Use trait aliases when they're stable, or add these bounds to every method.
+pub trait AsyncRead: tokio::io::AsyncRead + Unpin + Send {}
+impl AsyncRead for webtransport_quinn::RecvStream {}
+
+/// A decode error.
#[derive(Error, Debug)]
pub enum DecodeError {
#[error("unexpected end of buffer")]
@@ -14,6 +20,9 @@ pub enum DecodeError {
#[error("invalid type: {0:?}")]
InvalidType(VarInt),
+ #[error("varint bounds exceeded")]
+ BoundsExceeded(#[from] BoundsExceeded),
+
#[error("io error: {0}")]
IoError(#[from] std::io::Error),
}
diff --git a/moq-transport/src/coding/encode.rs b/moq-transport/src/coding/encode.rs
index cd0d928..65bd697 100644
--- a/moq-transport/src/coding/encode.rs
+++ b/moq-transport/src/coding/encode.rs
@@ -2,11 +2,20 @@ use super::BoundsExceeded;
use thiserror::Error;
+// I'm too lazy to add these trait bounds to every message type.
+// TODO Use trait aliases when they're stable, or add these bounds to every method.
+pub trait AsyncWrite: tokio::io::AsyncWrite + Unpin + Send {}
+impl AsyncWrite for webtransport_quinn::SendStream {}
+
+/// An encode error.
#[derive(Error, Debug)]
pub enum EncodeError {
#[error("varint too large")]
BoundsExceeded(#[from] BoundsExceeded),
+ #[error("invalid value")]
+ InvalidValue,
+
#[error("i/o error: {0}")]
IoError(#[from] std::io::Error),
}
diff --git a/moq-transport/src/coding/string.rs b/moq-transport/src/coding/string.rs
index 24c7bb0..3bc912c 100644
--- a/moq-transport/src/coding/string.rs
+++ b/moq-transport/src/coding/string.rs
@@ -1,20 +1,22 @@
use std::cmp::min;
+use crate::coding::{AsyncRead, AsyncWrite};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use webtransport_generic::{RecvStream, SendStream};
use crate::VarInt;
use super::{DecodeError, EncodeError};
-pub async fn encode_string(s: &str, w: &mut W) -> Result<(), EncodeError> {
+/// Encode a string with a varint length prefix.
+pub async fn encode_string(s: &str, w: &mut W) -> Result<(), EncodeError> {
let size = VarInt::try_from(s.len())?;
size.encode(w).await?;
w.write_all(s.as_ref()).await?;
Ok(())
}
-pub async fn decode_string(r: &mut R) -> Result {
+/// Decode a string with a varint length prefix.
+pub async fn decode_string(r: &mut R) -> Result {
let size = VarInt::decode(r).await?.into_inner();
let mut str = String::with_capacity(min(1024, size) as usize);
r.take(size).read_to_string(&mut str).await?;
diff --git a/moq-transport/src/coding/varint.rs b/moq-transport/src/coding/varint.rs
index 41a95ca..28542f3 100644
--- a/moq-transport/src/coding/varint.rs
+++ b/moq-transport/src/coding/varint.rs
@@ -5,14 +5,14 @@
use std::convert::{TryFrom, TryInto};
use std::fmt;
+use crate::coding::{AsyncRead, AsyncWrite};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use webtransport_generic::{RecvStream, SendStream};
use super::{DecodeError, EncodeError};
#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)]
-#[error("value too large for varint encoding")]
+#[error("value out of range")]
pub struct BoundsExceeded;
/// An integer less than 2^62
@@ -24,8 +24,12 @@ pub struct BoundsExceeded;
pub struct VarInt(u64);
impl VarInt {
+ /// The largest possible value.
pub const MAX: Self = Self((1 << 62) - 1);
+ /// The smallest possible value.
+ pub const ZERO: Self = Self(0);
+
/// Construct a `VarInt` infallibly using the largest available type.
/// Larger values need to use `try_from` instead.
pub const fn from_u32(x: u32) -> Self {
@@ -109,6 +113,45 @@ impl TryFrom for VarInt {
}
}
+impl TryFrom for u32 {
+ type Error = BoundsExceeded;
+
+ /// Succeeds iff `x` < 2^32
+ fn try_from(x: VarInt) -> Result {
+ if x.0 <= u32::MAX.into() {
+ Ok(x.0 as u32)
+ } else {
+ Err(BoundsExceeded)
+ }
+ }
+}
+
+impl TryFrom for u16 {
+ type Error = BoundsExceeded;
+
+ /// Succeeds iff `x` < 2^16
+ fn try_from(x: VarInt) -> Result {
+ if x.0 <= u16::MAX.into() {
+ Ok(x.0 as u16)
+ } else {
+ Err(BoundsExceeded)
+ }
+ }
+}
+
+impl TryFrom for u8 {
+ type Error = BoundsExceeded;
+
+ /// Succeeds iff `x` < 2^8
+ fn try_from(x: VarInt) -> Result {
+ if x.0 <= u8::MAX.into() {
+ Ok(x.0 as u8)
+ } else {
+ Err(BoundsExceeded)
+ }
+ }
+}
+
impl fmt::Debug for VarInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
@@ -122,7 +165,8 @@ impl fmt::Display for VarInt {
}
impl VarInt {
- pub async fn decode(r: &mut R) -> Result {
+ /// Decode a varint from the given reader.
+ pub async fn decode(r: &mut R) -> Result {
let mut buf = [0u8; 8];
r.read_exact(buf[0..1].as_mut()).await?;
@@ -149,7 +193,8 @@ impl VarInt {
Ok(Self(x))
}
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ /// Encode a varint to the given writer.
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
let x = self.0;
if x < 2u64.pow(6) {
w.write_u8(x as u8).await?;
@@ -166,3 +211,10 @@ impl VarInt {
Ok(())
}
}
+
+// This is a fork of quinn::VarInt.
+impl From for VarInt {
+ fn from(v: quinn::VarInt) -> Self {
+ Self(v.into_inner())
+ }
+}
diff --git a/moq-transport/src/error.rs b/moq-transport/src/error.rs
new file mode 100644
index 0000000..e147e23
--- /dev/null
+++ b/moq-transport/src/error.rs
@@ -0,0 +1,76 @@
+use thiserror::Error;
+
+use crate::VarInt;
+
+/// A MoQTransport error with an associated error code.
+#[derive(Copy, 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,
+
+ /// 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")]
+ Read,
+
+ /// An error occured while writing to the QUIC stream.
+ #[error("failed to write to stream")]
+ Write,
+
+ /// An unclassified error because I'm lazy. TODO classify these errors
+ #[error("unknown error")]
+ Unknown,
+}
+
+impl Error {
+ /// An integer code that is sent over the wire.
+ pub fn code(&self) -> u32 {
+ match self {
+ 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,
+ }
+ }
+
+ /// 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(_msg) => "role violation",
+ Self::Unknown => "unknown",
+ Self::Read => "read error",
+ Self::Write => "write error",
+ }
+ }
+}
diff --git a/moq-transport/src/lib.rs b/moq-transport/src/lib.rs
index cda369a..4516ea8 100644
--- a/moq-transport/src/lib.rs
+++ b/moq-transport/src/lib.rs
@@ -1,10 +1,20 @@
+//! An implementation of the MoQ Transport protocol.
+//!
+//! MoQ Transport is a pub/sub protocol over QUIC.
+//! While originally designed for live media, MoQ Transport is generic and can be used for other live applications.
+//! The specification is a work in progress and will change.
+//! See the [specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/) and [github](https://github.com/moq-wg/moq-transport) for any updates.
+//!
+//! **FORKED**: This is implementation makes extensive changes to the protocol.
+//! See [KIXEL_00](crate::setup::Version::KIXEL_00) for a list of differences.
+//! Many of these will get merged into the specification, so don't panic.
mod coding;
+mod error;
+
pub mod message;
-pub mod object;
+pub mod model;
pub mod session;
pub mod setup;
pub use coding::VarInt;
-pub use message::Message;
-pub use object::Object;
-pub use session::Session;
+pub use error::*;
diff --git a/moq-transport/src/message/announce.rs b/moq-transport/src/message/announce.rs
index 233f536..cdc3ddd 100644
--- a/moq-transport/src/message/announce.rs
+++ b/moq-transport/src/message/announce.rs
@@ -1,21 +1,22 @@
use crate::coding::{decode_string, encode_string, DecodeError, EncodeError};
-use webtransport_generic::{RecvStream, SendStream};
+use crate::coding::{AsyncRead, AsyncWrite};
+/// Sent by the publisher to announce the availability of a group of tracks.
#[derive(Clone, Debug)]
pub struct Announce {
// The track namespace
- pub track_namespace: String,
+ pub namespace: String,
}
impl Announce {
- pub async fn decode(r: &mut R) -> Result {
- let track_namespace = decode_string(r).await?;
- Ok(Self { track_namespace })
+ pub async fn decode(r: &mut R) -> Result {
+ let namespace = decode_string(r).await?;
+ Ok(Self { namespace })
}
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- encode_string(&self.track_namespace, w).await?;
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ encode_string(&self.namespace, w).await?;
Ok(())
}
}
diff --git a/moq-transport/src/message/announce_error.rs b/moq-transport/src/message/announce_error.rs
deleted file mode 100644
index 800235c..0000000
--- a/moq-transport/src/message/announce_error.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use crate::coding::{decode_string, encode_string, DecodeError, EncodeError, VarInt};
-
-use webtransport_generic::{RecvStream, SendStream};
-
-#[derive(Clone, Debug)]
-pub struct AnnounceError {
- // Echo back the namespace that was announced.
- // TODO Propose using an ID to save bytes.
- pub track_namespace: String,
-
- // An error code.
- pub code: VarInt,
-
- // An optional, human-readable reason.
- pub reason: String,
-}
-
-impl AnnounceError {
- pub async fn decode(r: &mut R) -> Result {
- let track_namespace = decode_string(r).await?;
- let code = VarInt::decode(r).await?;
- let reason = decode_string(r).await?;
-
- Ok(Self {
- track_namespace,
- code,
- reason,
- })
- }
-
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- encode_string(&self.track_namespace, w).await?;
- self.code.encode(w).await?;
- encode_string(&self.reason, w).await?;
-
- Ok(())
- }
-}
diff --git a/moq-transport/src/message/announce_ok.rs b/moq-transport/src/message/announce_ok.rs
index c09d2cc..de8b4d3 100644
--- a/moq-transport/src/message/announce_ok.rs
+++ b/moq-transport/src/message/announce_ok.rs
@@ -1,21 +1,20 @@
-use crate::coding::{decode_string, encode_string, DecodeError, EncodeError};
-
-use webtransport_generic::{RecvStream, SendStream};
+use crate::coding::{decode_string, encode_string, AsyncRead, AsyncWrite, DecodeError, EncodeError};
+/// Sent by the subscriber to accept an Announce.
#[derive(Clone, Debug)]
pub struct AnnounceOk {
// Echo back the namespace that was announced.
// TODO Propose using an ID to save bytes.
- pub track_namespace: String,
+ pub namespace: String,
}
impl AnnounceOk {
- pub async fn decode(r: &mut R) -> Result {
- let track_namespace = decode_string(r).await?;
- Ok(Self { track_namespace })
+ pub async fn decode(r: &mut R) -> Result {
+ let namespace = decode_string(r).await?;
+ Ok(Self { namespace })
}
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- encode_string(&self.track_namespace, w).await
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ encode_string(&self.namespace, w).await
}
}
diff --git a/moq-transport/src/message/announce_reset.rs b/moq-transport/src/message/announce_reset.rs
new file mode 100644
index 0000000..27e1326
--- /dev/null
+++ b/moq-transport/src/message/announce_reset.rs
@@ -0,0 +1,38 @@
+use crate::coding::{decode_string, encode_string, DecodeError, EncodeError, VarInt};
+
+use crate::coding::{AsyncRead, AsyncWrite};
+
+/// Sent by the subscriber to reject an Announce.
+#[derive(Clone, Debug)]
+pub struct AnnounceReset {
+ // Echo back the namespace that was reset
+ pub namespace: String,
+
+ // An error code.
+ pub code: u32,
+
+ // An optional, human-readable reason.
+ pub reason: String,
+}
+
+impl AnnounceReset {
+ pub async fn decode(r: &mut R) -> Result {
+ let namespace = decode_string(r).await?;
+ let code = VarInt::decode(r).await?.try_into()?;
+ let reason = decode_string(r).await?;
+
+ Ok(Self {
+ namespace,
+ code,
+ reason,
+ })
+ }
+
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ encode_string(&self.namespace, w).await?;
+ VarInt::from_u32(self.code).encode(w).await?;
+ encode_string(&self.reason, w).await?;
+
+ Ok(())
+ }
+}
diff --git a/moq-transport/src/message/announce_stop.rs b/moq-transport/src/message/announce_stop.rs
new file mode 100644
index 0000000..e184d90
--- /dev/null
+++ b/moq-transport/src/message/announce_stop.rs
@@ -0,0 +1,24 @@
+use crate::coding::{decode_string, encode_string, DecodeError, EncodeError};
+
+use crate::coding::{AsyncRead, AsyncWrite};
+
+/// Sent by the publisher to terminate an Announce.
+#[derive(Clone, Debug)]
+pub struct AnnounceStop {
+ // Echo back the namespace that was reset
+ pub namespace: String,
+}
+
+impl AnnounceStop {
+ pub async fn decode(r: &mut R) -> Result {
+ let namespace = decode_string(r).await?;
+
+ Ok(Self { namespace })
+ }
+
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ encode_string(&self.namespace, w).await?;
+
+ Ok(())
+ }
+}
diff --git a/moq-transport/src/message/go_away.rs b/moq-transport/src/message/go_away.rs
index 649c8b5..674cb5a 100644
--- a/moq-transport/src/message/go_away.rs
+++ b/moq-transport/src/message/go_away.rs
@@ -1,19 +1,20 @@
use crate::coding::{decode_string, encode_string, DecodeError, EncodeError};
-use webtransport_generic::{RecvStream, SendStream};
+use crate::coding::{AsyncRead, AsyncWrite};
+/// Sent by the server to indicate that the client should connect to a different server.
#[derive(Clone, Debug)]
pub struct GoAway {
pub url: String,
}
impl GoAway {
- pub async fn decode(r: &mut R) -> Result {
+ pub async fn decode(r: &mut R) -> Result {
let url = decode_string(r).await?;
Ok(Self { url })
}
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
encode_string(&self.url, w).await
}
}
diff --git a/moq-transport/src/message/mod.rs b/moq-transport/src/message/mod.rs
index b3d8f27..28ced81 100644
--- a/moq-transport/src/message/mod.rs
+++ b/moq-transport/src/message/mod.rs
@@ -1,48 +1,74 @@
+//! Low-level message sent over the wire, as defined in the specification.
+//!
+//! All of these messages are sent over a bidirectional QUIC stream.
+//! This introduces some head-of-line blocking but preserves ordering.
+//! The only exception are OBJECT "messages", which are sent over dedicated QUIC streams.
+//!
+//! Messages sent by the publisher:
+//! - [Announce]
+//! - [AnnounceReset]
+//! - [SubscribeOk]
+//! - [SubscribeReset]
+//! - [Object]
+//!
+//! Messages sent by the subscriber:
+//! - [Subscribe]
+//! - [SubscribeStop]
+//! - [AnnounceOk]
+//! - [AnnounceStop]
+//!
+//! Example flow:
+//! ```test
+//! -> ANNOUNCE namespace="foo"
+//! <- ANNOUNCE_OK namespace="foo"
+//! <- SUBSCRIBE id=0 namespace="foo" name="bar"
+//! -> SUBSCRIBE_OK id=0
+//! -> OBJECT id=0 sequence=69 priority=4 expires=30
+//! -> OBJECT id=0 sequence=70 priority=4 expires=30
+//! -> OBJECT id=0 sequence=70 priority=4 expires=30
+//! <- SUBSCRIBE_STOP id=0
+//! -> SUBSCRIBE_RESET id=0 code=206 reason="closed by peer"
+//! ```
mod announce;
-mod announce_error;
mod announce_ok;
+mod announce_reset;
+mod announce_stop;
mod go_away;
-mod receiver;
-mod sender;
+mod object;
mod subscribe;
-mod subscribe_error;
mod subscribe_ok;
+mod subscribe_reset;
+mod subscribe_stop;
pub use announce::*;
-pub use announce_error::*;
pub use announce_ok::*;
+pub use announce_reset::*;
+pub use announce_stop::*;
pub use go_away::*;
-pub use receiver::*;
-pub use sender::*;
+pub use object::*;
pub use subscribe::*;
-pub use subscribe_error::*;
pub use subscribe_ok::*;
+pub use subscribe_reset::*;
+pub use subscribe_stop::*;
use crate::coding::{DecodeError, EncodeError, VarInt};
use std::fmt;
-use webtransport_generic::{RecvStream, SendStream};
-
-// NOTE: This is forked from moq-transport-00.
-// 1. SETUP role indicates local support ("I can subscribe"), not remote support ("server must publish")
-// 2. SETUP_SERVER is id=2 to disambiguate
-// 3. messages do not have a specified length.
-// 4. messages are sent over a single bidrectional stream (after SETUP), not unidirectional streams.
-// 5. SUBSCRIBE specifies the track_id, not SUBSCRIBE_OK
-// 6. optional parameters are written in order, and zero when unset (setup, announce, subscribe)
+use crate::coding::{AsyncRead, AsyncWrite};
// Use a macro to generate the message types rather than copy-paste.
// This implements a decode/encode method that uses the specified type.
macro_rules! message_types {
{$($name:ident = $val:expr,)*} => {
+ /// All supported message types.
#[derive(Clone)]
pub enum Message {
$($name($name)),*
}
impl Message {
- pub async fn decode(r: &mut R) -> Result {
+ pub async fn decode(r: &mut R) -> Result {
let t = VarInt::decode(r).await?;
match t.into_inner() {
@@ -54,7 +80,7 @@ macro_rules! message_types {
}
}
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
match self {
$(Self::$name(ref m) => {
VarInt::from_u32($val).encode(w).await?;
@@ -62,6 +88,22 @@ macro_rules! message_types {
},)*
}
}
+
+ pub fn id(&self) -> VarInt {
+ match self {
+ $(Self::$name(_) => {
+ VarInt::from_u32($val)
+ },)*
+ }
+ }
+
+ pub fn name(&self) -> &'static str {
+ match self {
+ $(Self::$name(_) => {
+ stringify!($name)
+ },)*
+ }
+ }
}
$(impl From<$name> for Message {
@@ -89,9 +131,11 @@ message_types! {
// SetupServer = 0x2
Subscribe = 0x3,
SubscribeOk = 0x4,
- SubscribeError = 0x5,
+ SubscribeReset = 0x5,
+ SubscribeStop = 0x15,
Announce = 0x6,
AnnounceOk = 0x7,
- AnnounceError = 0x8,
+ AnnounceReset = 0x8,
+ AnnounceStop = 0x18,
GoAway = 0x10,
}
diff --git a/moq-transport/src/message/object.rs b/moq-transport/src/message/object.rs
new file mode 100644
index 0000000..a606f31
--- /dev/null
+++ b/moq-transport/src/message/object.rs
@@ -0,0 +1,70 @@
+use std::time;
+
+use crate::coding::{DecodeError, EncodeError, VarInt};
+
+use crate::coding::{AsyncRead, AsyncWrite};
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+/// Sent by the publisher as the header of each data stream.
+#[derive(Clone, Debug)]
+pub struct Object {
+ // An ID for this track.
+ // Proposal: https://github.com/moq-wg/moq-transport/issues/209
+ pub track: VarInt,
+
+ // The sequence number within the track.
+ pub sequence: VarInt,
+
+ // The priority, where **larger** values are sent first.
+ // Proposal: int32 instead of a varint.
+ pub priority: i32,
+
+ // Cache the object for at most this many seconds.
+ // Zero means never expire.
+ pub expires: Option,
+}
+
+impl Object {
+ pub async fn decode(r: &mut R) -> Result {
+ let typ = VarInt::decode(r).await?;
+ if typ.into_inner() != 0 {
+ return Err(DecodeError::InvalidType(typ));
+ }
+
+ // NOTE: size has been omitted
+
+ let track = VarInt::decode(r).await?;
+ let sequence = VarInt::decode(r).await?;
+ let priority = r.read_i32().await?; // big-endian
+ let expires = match VarInt::decode(r).await?.into_inner() {
+ 0 => None,
+ secs => Some(time::Duration::from_secs(secs)),
+ };
+
+ Ok(Self {
+ track,
+ sequence,
+ priority,
+ expires,
+ })
+ }
+
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ VarInt::ZERO.encode(w).await?;
+ self.track.encode(w).await?;
+ self.sequence.encode(w).await?;
+ w.write_i32(self.priority).await?;
+
+ // Round up if there's any decimal points.
+ let expires = match self.expires {
+ None => 0,
+ Some(time::Duration::ZERO) => return Err(EncodeError::InvalidValue), // there's no way of expressing zero currently.
+ Some(expires) if expires.subsec_nanos() > 0 => expires.as_secs() + 1,
+ Some(expires) => expires.as_secs(),
+ };
+
+ VarInt::try_from(expires)?.encode(w).await?;
+
+ Ok(())
+ }
+}
diff --git a/moq-transport/src/message/receiver.rs b/moq-transport/src/message/receiver.rs
deleted file mode 100644
index 29b0590..0000000
--- a/moq-transport/src/message/receiver.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use crate::{coding::DecodeError, message::Message};
-
-use webtransport_generic::RecvStream;
-
-pub struct Receiver {
- stream: R,
-}
-
-impl Receiver {
- pub fn new(stream: R) -> Self {
- Self { stream }
- }
-
- // Read the next full message from the stream.
- // NOTE: This is not cancellable; you must poll the future to completion.
- pub async fn recv(&mut self) -> Result {
- Message::decode(&mut self.stream).await
- }
-}
diff --git a/moq-transport/src/message/sender.rs b/moq-transport/src/message/sender.rs
deleted file mode 100644
index 5cefa2f..0000000
--- a/moq-transport/src/message/sender.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use crate::message::Message;
-
-use webtransport_generic::SendStream;
-
-pub struct Sender {
- stream: S,
-}
-
-impl Sender {
- pub fn new(stream: S) -> Self {
- Self { stream }
- }
-
- // Read the next full message from the stream.
- // NOTE: This is not cancellable; you must poll the future to completion.
- pub async fn send>(&mut self, msg: T) -> anyhow::Result<()> {
- let msg = msg.into();
- msg.encode(&mut self.stream).await?;
- Ok(())
- }
-}
diff --git a/moq-transport/src/message/subscribe.rs b/moq-transport/src/message/subscribe.rs
index 3d57cde..2f21a95 100644
--- a/moq-transport/src/message/subscribe.rs
+++ b/moq-transport/src/message/subscribe.rs
@@ -1,39 +1,38 @@
use crate::coding::{decode_string, encode_string, DecodeError, EncodeError, VarInt};
-use webtransport_generic::{RecvStream, SendStream};
+use crate::coding::{AsyncRead, AsyncWrite};
+/// Sent by the subscriber to request all future objects for the given track.
+///
+/// Objects will use the provided ID instead of the full track name, to save bytes.
#[derive(Clone, Debug)]
pub struct Subscribe {
// An ID we choose so we can map to the track_name.
// Proposal: https://github.com/moq-wg/moq-transport/issues/209
- pub track_id: VarInt,
+ pub id: VarInt,
// The track namespace.
- pub track_namespace: String,
+ pub namespace: String,
// The track name.
- pub track_name: String,
+ pub name: String,
}
impl Subscribe {
- pub async fn decode(r: &mut R) -> Result {
- let track_id = VarInt::decode(r).await?;
- let track_namespace = decode_string(r).await?;
- let track_name = decode_string(r).await?;
+ pub async fn decode(r: &mut R) -> Result {
+ let id = VarInt::decode(r).await?;
+ let namespace = decode_string(r).await?;
+ let name = decode_string(r).await?;
- Ok(Self {
- track_id,
- track_namespace,
- track_name,
- })
+ Ok(Self { id, namespace, name })
}
}
impl Subscribe {
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- self.track_id.encode(w).await?;
- encode_string(&self.track_namespace, w).await?;
- encode_string(&self.track_name, w).await?;
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ self.id.encode(w).await?;
+ encode_string(&self.namespace, w).await?;
+ encode_string(&self.name, w).await?;
Ok(())
}
diff --git a/moq-transport/src/message/subscribe_error.rs b/moq-transport/src/message/subscribe_error.rs
deleted file mode 100644
index c3a8702..0000000
--- a/moq-transport/src/message/subscribe_error.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use crate::coding::{decode_string, encode_string, DecodeError, EncodeError, VarInt};
-
-use webtransport_generic::{RecvStream, SendStream};
-
-#[derive(Clone, Debug)]
-pub struct SubscribeError {
- // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209
-
- // The ID for this track.
- pub track_id: VarInt,
-
- // An error code.
- pub code: VarInt,
-
- // An optional, human-readable reason.
- pub reason: String,
-}
-
-impl SubscribeError {
- pub async fn decode(r: &mut R) -> Result {
- let track_id = VarInt::decode(r).await?;
- let code = VarInt::decode(r).await?;
- let reason = decode_string(r).await?;
-
- Ok(Self { track_id, code, reason })
- }
-}
-
-impl SubscribeError {
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- self.track_id.encode(w).await?;
- self.code.encode(w).await?;
- encode_string(&self.reason, w).await?;
-
- Ok(())
- }
-}
diff --git a/moq-transport/src/message/subscribe_ok.rs b/moq-transport/src/message/subscribe_ok.rs
index ac88e97..bbc7f39 100644
--- a/moq-transport/src/message/subscribe_ok.rs
+++ b/moq-transport/src/message/subscribe_ok.rs
@@ -1,34 +1,26 @@
use crate::coding::{DecodeError, EncodeError, VarInt};
-use webtransport_generic::{RecvStream, SendStream};
+use crate::coding::{AsyncRead, AsyncWrite};
+/// Sent by the publisher to accept a Subscribe.
#[derive(Clone, Debug)]
pub struct SubscribeOk {
// NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209
// The ID for this track.
- pub track_id: VarInt,
-
- // The subscription will end after this duration has elapsed.
- // A value of zero is invalid.
- pub expires: Option,
+ pub id: VarInt,
}
impl SubscribeOk {
- pub async fn decode(r: &mut R) -> Result {
- let track_id = VarInt::decode(r).await?;
- let expires = VarInt::decode(r).await?;
- let expires = if expires.into_inner() == 0 { None } else { Some(expires) };
-
- Ok(Self { track_id, expires })
+ pub async fn decode(r: &mut R) -> Result {
+ let id = VarInt::decode(r).await?;
+ Ok(Self { id })
}
}
impl SubscribeOk {
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- self.track_id.encode(w).await?;
- self.expires.unwrap_or_default().encode(w).await?;
-
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ self.id.encode(w).await?;
Ok(())
}
}
diff --git a/moq-transport/src/message/subscribe_reset.rs b/moq-transport/src/message/subscribe_reset.rs
new file mode 100644
index 0000000..2daf9b2
--- /dev/null
+++ b/moq-transport/src/message/subscribe_reset.rs
@@ -0,0 +1,36 @@
+use crate::coding::{decode_string, encode_string, DecodeError, EncodeError, VarInt};
+
+use crate::coding::{AsyncRead, AsyncWrite};
+
+/// Sent by the publisher to reject a Subscribe.
+#[derive(Clone, Debug)]
+pub struct SubscribeReset {
+ // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209
+
+ // The ID for this subscription.
+ pub id: VarInt,
+
+ // An error code.
+ pub code: u32,
+
+ // An optional, human-readable reason.
+ pub reason: String,
+}
+
+impl SubscribeReset {
+ pub async fn decode(r: &mut R) -> Result {
+ let id = VarInt::decode(r).await?;
+ let code = VarInt::decode(r).await?.try_into()?;
+ let reason = decode_string(r).await?;
+
+ Ok(Self { id, code, reason })
+ }
+
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ self.id.encode(w).await?;
+ VarInt::from_u32(self.code).encode(w).await?;
+ encode_string(&self.reason, w).await?;
+
+ Ok(())
+ }
+}
diff --git a/moq-transport/src/message/subscribe_stop.rs b/moq-transport/src/message/subscribe_stop.rs
new file mode 100644
index 0000000..a1170d2
--- /dev/null
+++ b/moq-transport/src/message/subscribe_stop.rs
@@ -0,0 +1,26 @@
+use crate::coding::{DecodeError, EncodeError, VarInt};
+
+use crate::coding::{AsyncRead, AsyncWrite};
+
+/// Sent by the subscriber to terminate a Subscribe.
+#[derive(Clone, Debug)]
+pub struct SubscribeStop {
+ // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209
+
+ // The ID for this subscription.
+ pub id: VarInt,
+}
+
+impl SubscribeStop {
+ pub async fn decode(r: &mut R) -> Result {
+ let id = VarInt::decode(r).await?;
+ Ok(Self { id })
+ }
+}
+
+impl SubscribeStop {
+ pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
+ self.id.encode(w).await?;
+ Ok(())
+ }
+}
diff --git a/moq-transport/src/model/broadcast.rs b/moq-transport/src/model/broadcast.rs
new file mode 100644
index 0000000..652f105
--- /dev/null
+++ b/moq-transport/src/model/broadcast.rs
@@ -0,0 +1,211 @@
+//! A broadcast is a collection of tracks, split into two handles: [Publisher] and [Subscriber].
+//!
+//! The [Publisher] can create tracks, either manually or on request.
+//! 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].
+//!
+//! A [Subscriber] can request tracks by name.
+//! If the track already exists, it will be returned.
+//! If the track doesn't exist, it will be sent to [Unknown] to be handled.
+//! 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.
+use std::{
+ collections::{hash_map, HashMap, VecDeque},
+ fmt,
+ sync::Arc,
+};
+
+use crate::Error;
+
+use super::{track, Watch};
+
+/// Create a new broadcast.
+pub fn new() -> (Publisher, Subscriber) {
+ let state = Watch::new(State::default());
+
+ let publisher = Publisher::new(state.clone());
+ let subscriber = Subscriber::new(state);
+
+ (publisher, subscriber)
+}
+
+/// Dynamic information about the broadcast.
+#[derive(Debug)]
+struct State {
+ tracks: HashMap,
+ requested: VecDeque,
+ closed: Result<(), Error>,
+}
+
+impl State {
+ pub fn get(&self, name: &str) -> Result, Error> {
+ // Don't check closed, so we can return from cache.
+ Ok(self.tracks.get(name).cloned())
+ }
+
+ pub fn insert(&mut self, track: track::Subscriber) -> Result<(), Error> {
+ self.closed?;
+
+ match self.tracks.entry(track.name.clone()) {
+ hash_map::Entry::Occupied(_) => return Err(Error::Duplicate),
+ hash_map::Entry::Vacant(v) => v.insert(track),
+ };
+
+ Ok(())
+ }
+
+ pub fn request(&mut self, name: &str) -> Result {
+ self.closed?;
+
+ // Create a new track.
+ let (publisher, subscriber) = track::new(name);
+
+ // Insert the track into our Map so we deduplicate future requests.
+ self.tracks.insert(name.to_string(), subscriber.clone());
+
+ // Send the track to the Publisher to handle.
+ self.requested.push_back(publisher);
+
+ Ok(subscriber)
+ }
+
+ pub fn has_next(&self) -> Result {
+ // Check if there's any elements in the queue before checking closed.
+ if !self.requested.is_empty() {
+ return Ok(true);
+ }
+
+ self.closed?;
+ Ok(false)
+ }
+
+ pub fn next(&mut self) -> track::Publisher {
+ // We panic instead of erroring to avoid a nasty wakeup loop if you don't call has_next first.
+ self.requested.pop_front().expect("no entry in queue")
+ }
+
+ pub fn close(&mut self, err: Error) -> Result<(), Error> {
+ self.closed?;
+ self.closed = Err(err);
+ Ok(())
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ tracks: HashMap::new(),
+ closed: Ok(()),
+ requested: VecDeque::new(),
+ }
+ }
+}
+
+/// Publish new tracks for a broadcast by name.
+// TODO remove Clone
+#[derive(Clone)]
+pub struct Publisher {
+ state: Watch,
+ _dropped: Arc,
+}
+
+impl Publisher {
+ fn new(state: Watch) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+ Self { state, _dropped }
+ }
+
+ /// Create a new track with the given name, inserting it into the broadcast.
+ pub fn create_track(&mut self, name: &str) -> Result {
+ let (publisher, subscriber) = track::new(name);
+ self.state.lock_mut().insert(subscriber)?;
+ Ok(publisher)
+ }
+
+ /// Insert a track into the broadcast.
+ pub fn insert_track(&mut self, track: track::Subscriber) -> Result<(), Error> {
+ self.state.lock_mut().insert(track)
+ }
+
+ /// Block until the next track requested by a subscriber.
+ pub async fn next_track(&mut self) -> Result, Error> {
+ loop {
+ let notify = {
+ let state = self.state.lock();
+ if state.has_next()? {
+ return Ok(Some(state.into_mut().next()));
+ }
+
+ state.changed()
+ };
+
+ notify.await;
+ }
+ }
+
+ /// Close the broadcast with an error.
+ pub fn close(self, err: Error) -> Result<(), Error> {
+ self.state.lock_mut().close(err)
+ }
+}
+
+impl fmt::Debug for Publisher {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Publisher").field("state", &self.state).finish()
+ }
+}
+
+/// Subscribe to a broadcast by requesting tracks.
+///
+/// This can be cloned to create handles.
+#[derive(Clone)]
+pub struct Subscriber {
+ state: Watch,
+ _dropped: Arc,
+}
+
+impl Subscriber {
+ fn new(state: Watch) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+ Self { state, _dropped }
+ }
+
+ /// 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).
+ /// Otherwise, it will return [Error::NotFound].
+ pub fn get_track(&self, name: &str) -> Result {
+ let state = self.state.lock();
+ if let Some(track) = state.get(name)? {
+ return Ok(track);
+ }
+
+ // Request a new track if it does not exist.
+ state.into_mut().request(name)
+ }
+}
+
+impl fmt::Debug for Subscriber {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Subscriber").field("state", &self.state).finish()
+ }
+}
+
+// A handle that closes the broadcast when dropped:
+// - when all Subscribers are dropped or
+// - when Publisher and Unknown are dropped.
+struct Dropped {
+ state: Watch,
+}
+
+impl Dropped {
+ fn new(state: Watch) -> Self {
+ Self { state }
+ }
+}
+
+impl Drop for Dropped {
+ fn drop(&mut self) {
+ self.state.lock_mut().close(Error::Closed).ok();
+ }
+}
diff --git a/moq-transport/src/model/mod.rs b/moq-transport/src/model/mod.rs
new file mode 100644
index 0000000..aa56585
--- /dev/null
+++ b/moq-transport/src/model/mod.rs
@@ -0,0 +1,11 @@
+//! Allows a publisher to push updates, automatically caching and fanning it out to any subscribers.
+//!
+//! The naming scheme doesn't match the spec because it's vague and confusing.
+//! The hierarchy is: [broadcast] -> [track] -> [segment] -> [Bytes](bytes::Bytes)
+
+pub mod broadcast;
+pub mod segment;
+pub mod track;
+
+pub(crate) mod watch;
+pub(crate) use watch::*;
diff --git a/moq-transport/src/model/segment.rs b/moq-transport/src/model/segment.rs
new file mode 100644
index 0000000..d2db43a
--- /dev/null
+++ b/moq-transport/src/model/segment.rs
@@ -0,0 +1,215 @@
+//! A segment is a stream of bytes with a header, split into a [Publisher] and [Subscriber] handle.
+//!
+//! A [Publisher] writes an ordered stream of bytes in chunks.
+//! There's no framing, so these chunks can be of any size or position, and won't be maintained over the network.
+//!
+//! A [Subscriber] reads an ordered stream of bytes in chunks.
+//! 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)
+//!
+//! The segment is closed with [Error::Closed] when all publishers or subscribers are dropped.
+use core::fmt;
+use std::{ops::Deref, sync::Arc, time};
+
+use crate::{Error, VarInt};
+use bytes::Bytes;
+
+use super::Watch;
+
+/// Create a new segment with the given info.
+pub fn new(info: Info) -> (Publisher, Subscriber) {
+ let state = Watch::new(State::default());
+ let info = Arc::new(info);
+
+ let publisher = Publisher::new(state.clone(), info.clone());
+ let subscriber = Subscriber::new(state, info);
+
+ (publisher, subscriber)
+}
+
+/// Static information about the segment.
+#[derive(Debug)]
+pub struct Info {
+ // The sequence number of the segment within the track.
+ pub sequence: VarInt,
+
+ // The priority of the segment within the BROADCAST.
+ pub priority: i32,
+
+ // Cache the segment for at most this long.
+ pub expires: Option,
+}
+
+struct State {
+ // The data that has been received thus far.
+ data: Vec,
+
+ // Set when the publisher is dropped.
+ closed: Result<(), Error>,
+}
+
+impl State {
+ pub fn close(&mut self, err: Error) -> Result<(), Error> {
+ self.closed?;
+ self.closed = Err(err);
+ Ok(())
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ data: Vec::new(),
+ closed: Ok(()),
+ }
+ }
+}
+
+impl fmt::Debug for State {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ // We don't want to print out the contents, so summarize.
+ let size = self.data.iter().map(|chunk| chunk.len()).sum::();
+ let data = format!("size={} chunks={}", size, self.data.len());
+
+ f.debug_struct("State")
+ .field("data", &data)
+ .field("closed", &self.closed)
+ .finish()
+ }
+}
+
+/// Used to write data to a segment and notify subscribers.
+pub struct Publisher {
+ // Mutable segment state.
+ state: Watch,
+
+ // Immutable segment state.
+ info: Arc,
+
+ // Closes the segment when all Publishers are dropped.
+ _dropped: Arc,
+}
+
+impl Publisher {
+ fn new(state: Watch, info: Arc) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+ Self { state, info, _dropped }
+ }
+
+ /// Write a new chunk of bytes.
+ pub fn write_chunk(&mut self, data: Bytes) -> Result<(), Error> {
+ let mut state = self.state.lock_mut();
+ state.closed?;
+ state.data.push(data);
+ Ok(())
+ }
+
+ /// Close the segment with an error.
+ pub fn close(self, err: Error) -> Result<(), Error> {
+ self.state.lock_mut().close(err)
+ }
+}
+
+impl Deref for Publisher {
+ type Target = Info;
+
+ fn deref(&self) -> &Self::Target {
+ &self.info
+ }
+}
+
+impl fmt::Debug for Publisher {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Publisher")
+ .field("state", &self.state)
+ .field("info", &self.info)
+ .finish()
+ }
+}
+
+/// Notified when a segment has new data available.
+#[derive(Clone)]
+pub struct Subscriber {
+ // Modify the segment state.
+ state: Watch,
+
+ // Immutable segment state.
+ info: Arc,
+
+ // The number of chunks that we've read.
+ // NOTE: Cloned subscribers inherit this index, but then run in parallel.
+ index: usize,
+
+ // Dropped when all Subscribers are dropped.
+ _dropped: Arc,
+}
+
+impl Subscriber {
+ fn new(state: Watch, info: Arc) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+
+ Self {
+ state,
+ info,
+ index: 0,
+ _dropped,
+ }
+ }
+
+ /// Block until the next chunk of bytes is available.
+ pub async fn read_chunk(&mut self) -> Result, Error> {
+ loop {
+ let notify = {
+ let state = self.state.lock();
+ if self.index < state.data.len() {
+ let chunk = state.data[self.index].clone();
+ self.index += 1;
+ return Ok(Some(chunk));
+ }
+
+ match state.closed {
+ Err(Error::Closed) => return Ok(None),
+ Err(err) => return Err(err),
+ Ok(()) => state.changed(),
+ }
+ };
+
+ notify.await; // Try again when the state changes
+ }
+ }
+}
+
+impl Deref for Subscriber {
+ type Target = Info;
+
+ fn deref(&self) -> &Self::Target {
+ &self.info
+ }
+}
+
+impl fmt::Debug for Subscriber {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Subscriber")
+ .field("state", &self.state)
+ .field("info", &self.info)
+ .field("index", &self.index)
+ .finish()
+ }
+}
+
+struct Dropped {
+ // Modify the segment state.
+ state: Watch,
+}
+
+impl Dropped {
+ fn new(state: Watch) -> Self {
+ Self { state }
+ }
+}
+
+impl Drop for Dropped {
+ fn drop(&mut self) {
+ self.state.lock_mut().close(Error::Closed).ok();
+ }
+}
diff --git a/moq-transport/src/model/track.rs b/moq-transport/src/model/track.rs
new file mode 100644
index 0000000..02ad04a
--- /dev/null
+++ b/moq-transport/src/model/track.rs
@@ -0,0 +1,337 @@
+//! A track is a collection of semi-reliable and semi-ordered segments, split into a [Publisher] and [Subscriber] handle.
+//!
+//! A [Publisher] creates segments with a sequence number and priority.
+//! The sequest number is used to determine the order of segments, while the priority is used to determine which segment to transmit first.
+//! This may seem counter-intuitive, but is designed for live streaming where the newest segments may be higher priority.
+//! A cloned [Publisher] can be used to create segments in parallel, but will error if a duplicate sequence number is used.
+//!
+//! A [Subscriber] may not receive all segments in order or at all.
+//! These segments are meant to be transmitted over congested networks and the key to MoQ Tranport is to not block on them.
+//! 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).
+//!
+//! The track is closed with [Error::Closed] when all publishers or subscribers are dropped.
+
+use std::{collections::BinaryHeap, fmt, ops::Deref, sync::Arc, time};
+
+use indexmap::IndexMap;
+
+use super::{segment, Watch};
+use crate::{Error, VarInt};
+
+/// Create a track with the given name.
+pub fn new(name: &str) -> (Publisher, Subscriber) {
+ let state = Watch::new(State::default());
+ let info = Arc::new(Info { name: name.to_string() });
+
+ let publisher = Publisher::new(state.clone(), info.clone());
+ let subscriber = Subscriber::new(state, info);
+
+ (publisher, subscriber)
+}
+
+/// Static information about a track.
+#[derive(Debug)]
+pub struct Info {
+ pub name: String,
+}
+
+struct State {
+ // Store segments in received order so subscribers can detect changes.
+ // The key is the segment sequence, which could have gaps.
+ // A None value means the segment has expired.
+ lookup: IndexMap>,
+
+ // Store when segments will expire in a priority queue.
+ expires: BinaryHeap,
+
+ // The number of None entries removed from the start of the lookup.
+ pruned: usize,
+
+ // Set when the publisher is closed/dropped, or all subscribers are dropped.
+ closed: Result<(), Error>,
+}
+
+impl State {
+ pub fn close(&mut self, err: Error) -> Result<(), Error> {
+ self.closed?;
+ self.closed = Err(err);
+ Ok(())
+ }
+
+ pub fn insert(&mut self, segment: segment::Subscriber) -> Result<(), Error> {
+ self.closed?;
+
+ let entry = match self.lookup.entry(segment.sequence) {
+ indexmap::map::Entry::Occupied(_entry) => return Err(Error::Duplicate),
+ indexmap::map::Entry::Vacant(entry) => entry,
+ };
+
+ if let Some(expires) = segment.expires {
+ self.expires.push(SegmentExpiration {
+ sequence: segment.sequence,
+ expires: time::Instant::now() + expires,
+ });
+ }
+
+ entry.insert(Some(segment));
+
+ // Expire any existing segments on insert.
+ // This means if you don't insert then you won't expire... but it's probably fine since the cache won't grow.
+ // TODO Use a timer to expire segments at the correct time instead
+ self.expire();
+
+ Ok(())
+ }
+
+ // Try expiring any segments
+ pub fn expire(&mut self) {
+ let now = time::Instant::now();
+ while let Some(segment) = self.expires.peek() {
+ if segment.expires > now {
+ break;
+ }
+
+ // Update the entry to None while preserving the index.
+ match self.lookup.entry(segment.sequence) {
+ indexmap::map::Entry::Occupied(mut entry) => entry.insert(None),
+ indexmap::map::Entry::Vacant(_) => panic!("expired segment not found"),
+ };
+
+ self.expires.pop();
+ }
+
+ // Remove None entries from the start of the lookup.
+ while let Some((_, None)) = self.lookup.get_index(0) {
+ self.lookup.shift_remove_index(0);
+ self.pruned += 1;
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ lookup: Default::default(),
+ expires: Default::default(),
+ pruned: 0,
+ closed: Ok(()),
+ }
+ }
+}
+
+impl fmt::Debug for State {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("State")
+ .field("lookup", &self.lookup)
+ .field("pruned", &self.pruned)
+ .field("closed", &self.closed)
+ .finish()
+ }
+}
+
+/// Creates new segments for a track.
+pub struct Publisher {
+ state: Watch,
+ info: Arc,
+ _dropped: Arc,
+}
+
+impl Publisher {
+ fn new(state: Watch, info: Arc) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+ Self { state, info, _dropped }
+ }
+
+ /// Insert a new segment.
+ pub fn insert_segment(&mut self, segment: segment::Subscriber) -> Result<(), Error> {
+ self.state.lock_mut().insert(segment)
+ }
+
+ /// Create an insert a segment with the given info.
+ pub fn create_segment(&mut self, info: segment::Info) -> Result {
+ let (publisher, subscriber) = segment::new(info);
+ self.insert_segment(subscriber)?;
+ Ok(publisher)
+ }
+
+ /// Close the segment with an error.
+ pub fn close(self, err: Error) -> Result<(), Error> {
+ self.state.lock_mut().close(err)
+ }
+}
+
+impl Deref for Publisher {
+ type Target = Info;
+
+ fn deref(&self) -> &Self::Target {
+ &self.info
+ }
+}
+
+impl fmt::Debug for Publisher {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Publisher")
+ .field("state", &self.state)
+ .field("info", &self.info)
+ .finish()
+ }
+}
+
+/// Receives new segments for a track.
+#[derive(Clone)]
+pub struct Subscriber {
+ state: Watch,
+ info: Arc,
+
+ // The index of the next segment to return.
+ index: usize,
+
+ // If there are multiple segments to return, we put them in here to return them in priority order.
+ pending: BinaryHeap,
+
+ // Dropped when all subscribers are dropped.
+ _dropped: Arc,
+}
+
+impl Subscriber {
+ fn new(state: Watch, info: Arc) -> Self {
+ let _dropped = Arc::new(Dropped::new(state.clone()));
+ Self {
+ state,
+ info,
+ index: 0,
+ pending: Default::default(),
+ _dropped,
+ }
+ }
+
+ /// Block until the next segment arrives, or return None if the track is [Error::Closed].
+ pub async fn next_segment(&mut self) -> Result, Error> {
+ loop {
+ let notify = {
+ let state = self.state.lock();
+
+ // Get our adjusted index, which could be negative if we've removed more broadcasts than read.
+ let mut index = self.index.saturating_sub(state.pruned);
+
+ // Push all new segments into a priority queue.
+ while index < state.lookup.len() {
+ let (_, segment) = state.lookup.get_index(index).unwrap();
+
+ // Skip None values (expired segments).
+ // TODO These might actually be expired, so we should check the expiration time.
+ if let Some(segment) = segment {
+ self.pending.push(SegmentPriority(segment.clone()));
+ }
+
+ index += 1;
+ }
+
+ self.index = state.pruned + index;
+
+ // Return the higher priority segment.
+ if let Some(segment) = self.pending.pop() {
+ return Ok(Some(segment.0));
+ }
+
+ // Otherwise check if we need to return an error.
+ match state.closed {
+ Err(Error::Closed) => return Ok(None),
+ Err(err) => return Err(err),
+ Ok(()) => state.changed(),
+ }
+ };
+
+ notify.await
+ }
+ }
+}
+
+impl Deref for Subscriber {
+ type Target = Info;
+
+ fn deref(&self) -> &Self::Target {
+ &self.info
+ }
+}
+
+impl fmt::Debug for Subscriber {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Subscriber")
+ .field("state", &self.state)
+ .field("info", &self.info)
+ .field("index", &self.index)
+ .finish()
+ }
+}
+
+// Closes the track on Drop.
+struct Dropped {
+ state: Watch,
+}
+
+impl Dropped {
+ fn new(state: Watch) -> Self {
+ Self { state }
+ }
+}
+
+impl Drop for Dropped {
+ fn drop(&mut self) {
+ self.state.lock_mut().close(Error::Closed).ok();
+ }
+}
+
+// Used to order segments by expiration time.
+struct SegmentExpiration {
+ sequence: VarInt,
+ expires: time::Instant,
+}
+
+impl Ord for SegmentExpiration {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ // Reverse order so the earliest expiration is at the top of the heap.
+ other.expires.cmp(&self.expires)
+ }
+}
+
+impl PartialOrd for SegmentExpiration {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+}
+
+impl PartialEq for SegmentExpiration {
+ fn eq(&self, other: &Self) -> bool {
+ self.expires == other.expires
+ }
+}
+
+impl Eq for SegmentExpiration {}
+
+// Used to order segments by priority
+#[derive(Clone)]
+struct SegmentPriority(pub segment::Subscriber);
+
+impl Ord for SegmentPriority {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ // Reverse order so the highest priority is at the top of the heap.
+ // TODO I let CodePilot generate this code so yolo
+ other.0.priority.cmp(&self.0.priority)
+ }
+}
+
+impl PartialOrd for SegmentPriority {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+}
+
+impl PartialEq for SegmentPriority {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.priority == other.0.priority
+ }
+}
+
+impl Eq for SegmentPriority {}
diff --git a/moq-transport/src/model/watch.rs b/moq-transport/src/model/watch.rs
new file mode 100644
index 0000000..93c8475
--- /dev/null
+++ b/moq-transport/src/model/watch.rs
@@ -0,0 +1,180 @@
+use std::{
+ fmt,
+ future::Future,
+ ops::{Deref, DerefMut},
+ pin::Pin,
+ sync::{Arc, Mutex, MutexGuard},
+ task,
+};
+
+struct State {
+ value: T,
+ wakers: Vec,
+ epoch: usize,
+}
+
+impl State {
+ pub fn new(value: T) -> Self {
+ Self {
+ value,
+ wakers: Vec::new(),
+ epoch: 0,
+ }
+ }
+
+ pub fn register(&mut self, waker: &task::Waker) {
+ self.wakers.retain(|existing| !existing.will_wake(waker));
+ self.wakers.push(waker.clone());
+ }
+
+ pub fn notify(&mut self) {
+ self.epoch += 1;
+ for waker in self.wakers.drain(..) {
+ waker.wake();
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self::new(T::default())
+ }
+}
+
+impl fmt::Debug for State {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.value.fmt(f)
+ }
+}
+
+pub struct Watch {
+ state: Arc>>,
+}
+
+impl Watch {
+ pub fn new(initial: T) -> Self {
+ let state = Arc::new(Mutex::new(State::new(initial)));
+ Self { state }
+ }
+
+ pub fn lock(&self) -> WatchRef {
+ WatchRef {
+ state: self.state.clone(),
+ lock: self.state.lock().unwrap(),
+ }
+ }
+
+ pub fn lock_mut(&self) -> WatchMut {
+ WatchMut {
+ lock: self.state.lock().unwrap(),
+ }
+ }
+}
+
+impl Clone for Watch {
+ fn clone(&self) -> Self {
+ Self {
+ state: self.state.clone(),
+ }
+ }
+}
+
+impl Default for Watch {
+ fn default() -> Self {
+ Self::new(T::default())
+ }
+}
+
+impl fmt::Debug for Watch {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.state.try_lock() {
+ Ok(lock) => lock.value.fmt(f),
+ Err(_) => write!(f, ""),
+ }
+ }
+}
+
+pub struct WatchRef<'a, T> {
+ state: Arc>>,
+ lock: MutexGuard<'a, State>,
+}
+
+impl<'a, T> WatchRef<'a, T> {
+ // Release the lock and wait for a notification when next updated.
+ pub fn changed(self) -> WatchChanged {
+ WatchChanged {
+ state: self.state,
+ epoch: self.lock.epoch,
+ }
+ }
+
+ // Upgrade to a mutable references that automatically calls notify on drop.
+ pub fn into_mut(self) -> WatchMut<'a, T> {
+ WatchMut { lock: self.lock }
+ }
+}
+
+impl<'a, T> Deref for WatchRef<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.lock.value
+ }
+}
+
+impl<'a, T: fmt::Debug> fmt::Debug for WatchRef<'a, T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.lock.fmt(f)
+ }
+}
+
+pub struct WatchMut<'a, T> {
+ lock: MutexGuard<'a, State>,
+}
+
+impl<'a, T> Deref for WatchMut<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.lock.value
+ }
+}
+
+impl<'a, T> DerefMut for WatchMut<'a, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.lock.value
+ }
+}
+
+impl<'a, T> Drop for WatchMut<'a, T> {
+ fn drop(&mut self) {
+ self.lock.notify();
+ }
+}
+
+impl<'a, T: fmt::Debug> fmt::Debug for WatchMut<'a, T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.lock.fmt(f)
+ }
+}
+
+pub struct WatchChanged {
+ state: Arc>>,
+ epoch: usize,
+}
+
+impl Future for WatchChanged {
+ type Output = ();
+
+ fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll {
+ // TODO is there an API we can make that doesn't drop this lock?
+ let mut state = self.state.lock().unwrap();
+
+ if state.epoch > self.epoch {
+ task::Poll::Ready(())
+ } else {
+ state.register(cx.waker());
+ task::Poll::Pending
+ }
+ }
+}
diff --git a/moq-transport/src/object/mod.rs b/moq-transport/src/object/mod.rs
deleted file mode 100644
index e017666..0000000
--- a/moq-transport/src/object/mod.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-mod receiver;
-mod sender;
-
-pub use receiver::*;
-pub use sender::*;
-
-use crate::coding::{DecodeError, EncodeError, VarInt};
-
-use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use webtransport_generic::{RecvStream, SendStream};
-
-#[derive(Clone, Debug)]
-pub struct Object {
- // An ID for this track.
- // Proposal: https://github.com/moq-wg/moq-transport/issues/209
- pub track: VarInt,
-
- // The group sequence number.
- pub group: VarInt,
-
- // The object sequence number.
- pub sequence: VarInt,
-
- // The priority/send order.
- // Proposal: int32 instead of a varint.
- pub send_order: i32,
-}
-
-impl Object {
- pub async fn decode(r: &mut R) -> Result {
- let typ = VarInt::decode(r).await?;
- if typ.into_inner() != 0 {
- return Err(DecodeError::InvalidType(typ));
- }
-
- // NOTE: size has been omitted
-
- let track = VarInt::decode(r).await?;
- let group = VarInt::decode(r).await?;
- let sequence = VarInt::decode(r).await?;
- let send_order = r.read_i32().await?; // big-endian
-
- Ok(Self {
- track,
- group,
- sequence,
- send_order,
- })
- }
-
- pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> {
- VarInt::from_u32(0).encode(w).await?;
- self.track.encode(w).await?;
- self.group.encode(w).await?;
- self.sequence.encode(w).await?;
- w.write_i32(self.send_order).await?;
-
- Ok(())
- }
-}
diff --git a/moq-transport/src/object/receiver.rs b/moq-transport/src/object/receiver.rs
deleted file mode 100644
index 521efb5..0000000
--- a/moq-transport/src/object/receiver.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use crate::Object;
-
-use anyhow::Context;
-
-use tokio::task::JoinSet;
-
-use webtransport_generic::Session;
-
-pub struct Receiver {
- session: S,
-
- // Streams that we've accepted but haven't read the header from yet.
- streams: JoinSet>,
-}
-
-impl Receiver {
- pub fn new(session: S) -> Self {
- Self {
- session,
- streams: JoinSet::new(),
- }
- }
-
- pub async fn recv(&mut self) -> anyhow::Result<(Object, S::RecvStream)> {
- loop {
- tokio::select! {
- res = self.session.accept_uni() => {
- let stream = res.context("failed to accept stream")?;
- self.streams.spawn(async move { Self::read(stream).await });
- },
- res = self.streams.join_next(), if !self.streams.is_empty() => {
- return res.unwrap().context("failed to run join set")?;
- }
- }
- }
- }
-
- async fn read(mut stream: S::RecvStream) -> anyhow::Result<(Object, S::RecvStream)> {
- let header = Object::decode(&mut stream).await?;
- Ok((header, stream))
- }
-}
diff --git a/moq-transport/src/object/sender.rs b/moq-transport/src/object/sender.rs
deleted file mode 100644
index d075658..0000000
--- a/moq-transport/src/object/sender.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use anyhow::Context;
-
-use crate::Object;
-
-use webtransport_generic::{SendStream, Session};
-
-// Allow this to be cloned so we can have multiple senders.
-#[derive(Clone)]
-pub struct Sender {
- // The session.
- session: S,
-}
-
-impl Sender {
- pub fn new(session: S) -> Self {
- Self { session }
- }
-
- pub async fn open(&mut self, object: Object) -> anyhow::Result {
- let mut stream = self.session.open_uni().await.context("failed to open uni stream")?;
-
- stream.set_priority(object.send_order);
- object.encode(&mut stream).await.context("failed to write header")?;
-
- // log::info!("created stream: {:?}", header);
-
- Ok(stream)
- }
-}
diff --git a/moq-transport/src/session.rs b/moq-transport/src/session.rs
deleted file mode 100644
index 96e95ae..0000000
--- a/moq-transport/src/session.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use anyhow::Context;
-
-use crate::{message, object, setup};
-use webtransport_generic::Session as WTSession;
-
-pub struct Session {
- pub send_control: message::Sender,
- pub recv_control: message::Receiver,
- pub send_objects: object::Sender,
- pub recv_objects: object::Receiver,
-}
-
-impl Session {
- /// Called by a server with an established WebTransport session.
- // TODO close the session with an error code
- pub async fn accept(session: S, role: setup::Role) -> anyhow::Result {
- let (mut send, mut recv) = session.accept_bi().await.context("failed to accept bidi stream")?;
-
- let setup_client = setup::Client::decode(&mut recv)
- .await
- .context("failed to read CLIENT SETUP")?;
-
- setup_client
- .versions
- .iter()
- .find(|version| **version == setup::Version::DRAFT_00)
- .context("no supported versions")?;
-
- let setup_server = setup::Server {
- role,
- version: setup::Version::DRAFT_00,
- };
-
- setup_server
- .encode(&mut send)
- .await
- .context("failed to send setup server")?;
-
- let send_control = message::Sender::new(send);
- let recv_control = message::Receiver::new(recv);
-
- let send_objects = object::Sender::new(session.clone());
- let recv_objects = object::Receiver::new(session.clone());
-
- Ok(Session {
- send_control,
- recv_control,
- send_objects,
- recv_objects,
- })
- }
-
- /// Called by a client with an established WebTransport session.
- pub async fn connect(session: S, role: setup::Role) -> anyhow::Result {
- let (mut send, mut recv) = session.open_bi().await.context("failed to oen bidi stream")?;
-
- let setup_client = setup::Client {
- role,
- versions: vec![setup::Version::DRAFT_00].into(),
- path: "".to_string(),
- };
-
- setup_client
- .encode(&mut send)
- .await
- .context("failed to send SETUP CLIENT")?;
-
- let setup_server = setup::Server::decode(&mut recv).await.context("failed to read SETUP")?;
-
- if setup_server.version != setup::Version::DRAFT_00 {
- anyhow::bail!("unsupported version: {:?}", setup_server.version);
- }
-
- let send_control = message::Sender::new(send);
- let recv_control = message::Receiver::new(recv);
-
- let send_objects = object::Sender::new(session.clone());
- let recv_objects = object::Receiver::new(session.clone());
-
- Ok(Session {
- send_control,
- recv_control,
- send_objects,
- recv_objects,
- })
- }
-}
diff --git a/moq-transport/src/session/client.rs b/moq-transport/src/session/client.rs
new file mode 100644
index 0000000..c9ceffc
--- /dev/null
+++ b/moq-transport/src/session/client.rs
@@ -0,0 +1,62 @@
+use super::{Publisher, Subscriber};
+use crate::{model::broadcast, setup};
+use webtransport_quinn::{RecvStream, SendStream, Session};
+
+use anyhow::Context;
+
+/// An endpoint that connects to a URL to publish and/or consume live streams.
+pub struct Client {}
+
+impl Client {
+ /// Connect using an established WebTransport session, performing the MoQ handshake as a publisher.
+ pub async fn publisher(session: Session, source: broadcast::Subscriber) -> anyhow::Result {
+ let control = Self::send_setup(&session, setup::Role::Publisher).await?;
+
+ let publisher = Publisher::new(session, control, source);
+ Ok(publisher)
+ }
+
+ /// Connect using an established WebTransport session, performing the MoQ handshake as a subscriber.
+ pub async fn subscriber(session: Session, source: broadcast::Publisher) -> anyhow::Result {
+ let control = Self::send_setup(&session, setup::Role::Subscriber).await?;
+
+ let subscriber = Subscriber::new(session, control, source);
+ Ok(subscriber)
+ }
+
+ // TODO support performing both roles
+ /*
+ pub async fn connect(self) -> anyhow::Result<(Publisher, Subscriber)> {
+ self.connect_role(setup::Role::Both).await
+ }
+ */
+
+ async fn send_setup(session: &Session, role: setup::Role) -> anyhow::Result<(SendStream, RecvStream)> {
+ let mut control = session.open_bi().await.context("failed to oen bidi stream")?;
+
+ let client = setup::Client {
+ role,
+ versions: vec![setup::Version::KIXEL_00].into(),
+ };
+
+ client
+ .encode(&mut control.0)
+ .await
+ .context("failed to send SETUP CLIENT")?;
+
+ let server = setup::Server::decode(&mut control.1)
+ .await
+ .context("failed to read SETUP")?;
+
+ if server.version != setup::Version::KIXEL_00 {
+ anyhow::bail!("unsupported version: {:?}", server.version);
+ }
+
+ // Make sure the server replied with the
+ if !client.role.is_compatible(server.role) {
+ anyhow::bail!("incompatible roles: client={:?} server={:?}", client.role, server.role);
+ }
+
+ Ok(control)
+ }
+}
diff --git a/moq-transport/src/session/control.rs b/moq-transport/src/session/control.rs
new file mode 100644
index 0000000..65295a7
--- /dev/null
+++ b/moq-transport/src/session/control.rs
@@ -0,0 +1,35 @@
+// A helper class to guard sending control messages behind a Mutex.
+
+use std::{fmt, sync::Arc};
+
+use tokio::sync::Mutex;
+use webtransport_quinn::{RecvStream, SendStream};
+
+use crate::{message::Message, Error};
+
+#[derive(Debug, Clone)]
+pub(crate) struct Control {
+ send: Arc>,
+ recv: Arc>,
+}
+
+impl Control {
+ pub fn new(send: SendStream, recv: RecvStream) -> Self {
+ Self {
+ send: Arc::new(Mutex::new(send)),
+ recv: Arc::new(Mutex::new(recv)),
+ }
+ }
+
+ pub async fn send