From 88542e266cf927039b950cec65cb0f3a91e011cd Mon Sep 17 00:00:00 2001 From: kixelated Date: Fri, 15 Sep 2023 12:06:28 -0700 Subject: [PATCH] Major moq-transport API simplification (#68) Exponentially easier to use moq-transport as there's no message handling required. This is a BREAKING CHANGE. --- .dockerignore | 5 +- .github/logo.svg | 348 +++++++++++ Cargo.lock | 597 +++++++++---------- Cargo.toml | 8 +- Dockerfile | 47 +- README.md | 71 ++- {cert => dev}/.gitignore | 3 +- cert/generate => dev/cert | 0 {cert => dev}/go.mod | 0 {cert => dev}/go.sum | 0 dev/pub | 25 + dev/relay | 13 + entrypoint.sh | 8 - moq-pub/Cargo.toml | 12 +- moq-pub/README.md | 25 +- moq-pub/src/cli.rs | 48 +- moq-pub/src/log_viewer.rs | 39 -- moq-pub/src/main.rs | 74 +-- moq-pub/src/media.rs | 179 ++---- moq-pub/src/media_runner.rs | 159 ----- moq-pub/src/session_runner.rs | 122 ---- {moq-quinn => moq-relay}/Cargo.toml | 10 +- moq-relay/README.md | 17 + moq-relay/src/config.rs | 23 + {moq-quinn => moq-relay}/src/main.rs | 52 +- {moq-quinn => moq-relay}/src/server.rs | 73 +-- moq-relay/src/session.rs | 96 +++ moq-transport/Cargo.toml | 10 +- moq-transport/README.md | 10 + moq-transport/src/coding/decode.rs | 11 +- moq-transport/src/coding/encode.rs | 9 + moq-transport/src/coding/string.rs | 8 +- moq-transport/src/coding/varint.rs | 60 +- moq-transport/src/error.rs | 76 +++ moq-transport/src/lib.rs | 18 +- moq-transport/src/message/announce.rs | 15 +- moq-transport/src/message/announce_error.rs | 38 -- moq-transport/src/message/announce_ok.rs | 17 +- moq-transport/src/message/announce_reset.rs | 38 ++ moq-transport/src/message/announce_stop.rs | 24 + moq-transport/src/message/go_away.rs | 7 +- moq-transport/src/message/mod.rs | 86 ++- moq-transport/src/message/object.rs | 70 +++ moq-transport/src/message/receiver.rs | 19 - moq-transport/src/message/sender.rs | 21 - moq-transport/src/message/subscribe.rs | 33 +- moq-transport/src/message/subscribe_error.rs | 37 -- moq-transport/src/message/subscribe_ok.rs | 24 +- moq-transport/src/message/subscribe_reset.rs | 36 ++ moq-transport/src/message/subscribe_stop.rs | 26 + moq-transport/src/model/broadcast.rs | 211 +++++++ moq-transport/src/model/mod.rs | 11 + moq-transport/src/model/segment.rs | 215 +++++++ moq-transport/src/model/track.rs | 337 +++++++++++ moq-transport/src/model/watch.rs | 180 ++++++ moq-transport/src/object/mod.rs | 60 -- moq-transport/src/object/receiver.rs | 42 -- moq-transport/src/object/sender.rs | 29 - moq-transport/src/session.rs | 87 --- moq-transport/src/session/client.rs | 62 ++ moq-transport/src/session/control.rs | 35 ++ moq-transport/src/session/mod.rs | 25 + moq-transport/src/session/publisher.rs | 189 ++++++ moq-transport/src/session/server.rs | 100 ++++ moq-transport/src/session/subscriber.rs | 152 +++++ moq-transport/src/setup/client.rs | 28 +- moq-transport/src/setup/mod.rs | 6 + moq-transport/src/setup/role.rs | 16 +- moq-transport/src/setup/server.rs | 14 +- moq-transport/src/setup/version.rs | 66 +- moq-warp/Cargo.toml | 24 - moq-warp/src/lib.rs | 2 - moq-warp/src/model/broadcast.rs | 64 -- moq-warp/src/model/fragment.rs | 5 - moq-warp/src/model/mod.rs | 5 - moq-warp/src/model/segment.rs | 66 -- moq-warp/src/model/track.rs | 101 ---- moq-warp/src/model/watch.rs | 135 ----- moq-warp/src/relay/broker.rs | 76 --- moq-warp/src/relay/contribute.rs | 308 ---------- moq-warp/src/relay/distribute.rs | 205 ------- moq-warp/src/relay/message.rs | 127 ---- moq-warp/src/relay/mod.rs | 8 - moq-warp/src/relay/session.rs | 37 -- 84 files changed, 3172 insertions(+), 2603 deletions(-) create mode 100644 .github/logo.svg rename {cert => dev}/.gitignore (50%) rename cert/generate => dev/cert (100%) rename {cert => dev}/go.mod (100%) rename {cert => dev}/go.sum (100%) create mode 100755 dev/pub create mode 100755 dev/relay delete mode 100755 entrypoint.sh delete mode 100644 moq-pub/src/log_viewer.rs delete mode 100644 moq-pub/src/media_runner.rs delete mode 100644 moq-pub/src/session_runner.rs rename {moq-quinn => moq-relay}/Cargo.toml (82%) create mode 100644 moq-relay/README.md create mode 100644 moq-relay/src/config.rs rename {moq-quinn => moq-relay}/src/main.rs (55%) rename {moq-quinn => moq-relay}/src/server.rs (51%) create mode 100644 moq-relay/src/session.rs create mode 100644 moq-transport/README.md create mode 100644 moq-transport/src/error.rs delete mode 100644 moq-transport/src/message/announce_error.rs create mode 100644 moq-transport/src/message/announce_reset.rs create mode 100644 moq-transport/src/message/announce_stop.rs create mode 100644 moq-transport/src/message/object.rs delete mode 100644 moq-transport/src/message/receiver.rs delete mode 100644 moq-transport/src/message/sender.rs delete mode 100644 moq-transport/src/message/subscribe_error.rs create mode 100644 moq-transport/src/message/subscribe_reset.rs create mode 100644 moq-transport/src/message/subscribe_stop.rs create mode 100644 moq-transport/src/model/broadcast.rs create mode 100644 moq-transport/src/model/mod.rs create mode 100644 moq-transport/src/model/segment.rs create mode 100644 moq-transport/src/model/track.rs create mode 100644 moq-transport/src/model/watch.rs delete mode 100644 moq-transport/src/object/mod.rs delete mode 100644 moq-transport/src/object/receiver.rs delete mode 100644 moq-transport/src/object/sender.rs delete mode 100644 moq-transport/src/session.rs create mode 100644 moq-transport/src/session/client.rs create mode 100644 moq-transport/src/session/control.rs create mode 100644 moq-transport/src/session/mod.rs create mode 100644 moq-transport/src/session/publisher.rs create mode 100644 moq-transport/src/session/server.rs create mode 100644 moq-transport/src/session/subscriber.rs delete mode 100644 moq-warp/Cargo.toml delete mode 100644 moq-warp/src/lib.rs delete mode 100644 moq-warp/src/model/broadcast.rs delete mode 100644 moq-warp/src/model/fragment.rs delete mode 100644 moq-warp/src/model/mod.rs delete mode 100644 moq-warp/src/model/segment.rs delete mode 100644 moq-warp/src/model/track.rs delete mode 100644 moq-warp/src/model/watch.rs delete mode 100644 moq-warp/src/relay/broker.rs delete mode 100644 moq-warp/src/relay/contribute.rs delete mode 100644 moq-warp/src/relay/distribute.rs delete mode 100644 moq-warp/src/relay/message.rs delete mode 100644 moq-warp/src/relay/mod.rs delete mode 100644 moq-warp/src/relay/session.rs 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 + fmt::Debug>(&self, msg: T) -> Result<(), Error> { + let mut stream = self.send.lock().await; + log::info!("sending message: {:?}", msg); + msg.into().encode(&mut *stream).await.map_err(|_e| Error::Write) + } + + // It's likely a mistake to call this from two different tasks, but it's easier to just support it. + pub async fn recv(&self) -> Result { + let mut stream = self.recv.lock().await; + Message::decode(&mut *stream).await.map_err(|_e| Error::Read) + } +} diff --git a/moq-transport/src/session/mod.rs b/moq-transport/src/session/mod.rs new file mode 100644 index 0000000..d6e7ca2 --- /dev/null +++ b/moq-transport/src/session/mod.rs @@ -0,0 +1,25 @@ +//! A MoQ Transport session, on top of a WebTransport session, on top of a QUIC connection. +//! +//! The handshake is relatively simple but split into different steps. +//! All of these handshakes slightly differ depending on if the endpoint is a client or server. +//! 1. Complete the QUIC handhake. +//! 2. Complete the WebTransport handshake. +//! 3. Complete the MoQ handshake. +//! +//! Use [Client] or [Server] for the MoQ handshake depending on the endpoint. +//! Then, decide if you want to create a [Publisher] or [Subscriber], or both (TODO). +//! +//! A [Publisher] can announce broadcasts, which will automatically be served over the network. +//! A [Subscriber] can subscribe to broadcasts, which will automatically be served over the network. + +mod client; +mod control; +mod publisher; +mod server; +mod subscriber; + +pub use client::*; +pub(crate) use control::*; +pub use publisher::*; +pub use server::*; +pub use subscriber::*; diff --git a/moq-transport/src/session/publisher.rs b/moq-transport/src/session/publisher.rs new file mode 100644 index 0000000..bffdf6c --- /dev/null +++ b/moq-transport/src/session/publisher.rs @@ -0,0 +1,189 @@ +use std::{ + collections::{hash_map, HashMap}, + sync::{Arc, Mutex}, +}; + +use tokio::task::AbortHandle; +use webtransport_quinn::{RecvStream, SendStream, Session}; + +use crate::{ + message, + message::Message, + model::{broadcast, segment, track}, + Error, VarInt, +}; + +use super::Control; + +/// Serves broadcasts over the network, automatically handling subscriptions and caching. +// TODO Clone specific fields when a task actually needs it. +#[derive(Clone, Debug)] +pub struct Publisher { + // A map of active subscriptions, containing an abort handle to cancel them. + subscribes: Arc>>, + webtransport: Session, + control: Control, + source: broadcast::Subscriber, +} + +impl Publisher { + pub(crate) fn new(webtransport: Session, control: (SendStream, RecvStream), source: broadcast::Subscriber) -> Self { + let control = Control::new(control.0, control.1); + + Self { + webtransport, + subscribes: Default::default(), + control, + source, + } + } + + // TODO Serve a broadcast without sending an ANNOUNCE. + // fn serve(&mut self, broadcast: broadcast::Subscriber) -> Result<(), Error> { + + // TODO Wait until the next subscribe that doesn't route to an ANNOUNCE. + // pub async fn subscribed(&mut self) -> Result { + + pub async fn run(mut self) -> Result<(), Error> { + loop { + tokio::select! { + _stream = self.webtransport.accept_uni() => { + return Err(Error::Role(VarInt::ZERO)); + } + // NOTE: this is not cancel safe, but it's fine since the other branch is a fatal error. + msg = self.control.recv() => { + let msg = msg.map_err(|_x| Error::Read)?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg).await { + log::warn!("message error: {:?} {:?}", err, msg); + } + } + } + } + } + + async fn recv_message(&mut self, msg: &Message) -> Result<(), Error> { + match msg { + Message::AnnounceOk(msg) => self.recv_announce_ok(msg).await, + Message::AnnounceStop(msg) => self.recv_announce_stop(msg).await, + Message::Subscribe(msg) => self.recv_subscribe(msg).await, + Message::SubscribeStop(msg) => self.recv_subscribe_stop(msg).await, + _ => Err(Error::Role(msg.id())), + } + } + + async fn recv_announce_ok(&mut self, _msg: &message::AnnounceOk) -> Result<(), Error> { + // We didn't send an announce. + Err(Error::NotFound) + } + + async fn recv_announce_stop(&mut self, _msg: &message::AnnounceStop) -> Result<(), Error> { + // We didn't send an announce. + Err(Error::NotFound) + } + + async fn recv_subscribe(&mut self, msg: &message::Subscribe) -> Result<(), Error> { + // Assume that the subscribe ID is unique for now. + let abort = match self.start_subscribe(msg.clone()) { + Ok(abort) => abort, + Err(err) => return self.reset_subscribe(msg.id, err).await, + }; + + // Insert the abort handle into the lookup table. + match self.subscribes.lock().unwrap().entry(msg.id) { + hash_map::Entry::Occupied(_) => return Err(Error::Duplicate), // TODO fatal, because we already started the task + hash_map::Entry::Vacant(entry) => entry.insert(abort), + }; + + self.control.send(message::SubscribeOk { id: msg.id }).await + } + + async fn reset_subscribe(&mut self, id: VarInt, err: Error) -> Result<(), Error> { + let msg = message::SubscribeReset { + id, + code: err.code(), + reason: err.reason().to_string(), + }; + + self.control.send(msg).await + } + + fn start_subscribe(&mut self, msg: message::Subscribe) -> Result { + // We currently don't use the namespace field in SUBSCRIBE + if !msg.namespace.is_empty() { + return Err(Error::NotFound); + } + + let mut track = self.source.get_track(&msg.name)?; + + // TODO only clone the fields we need + let mut this = self.clone(); + + let handle = tokio::spawn(async move { + log::info!("serving track: name={}", track.name); + + let res = this.run_subscribe(msg.id, &mut track).await; + if let Err(err) = &res { + log::warn!("failed to serve track: name={} err={:?}", track.name, err); + } + + // Make sure we send a reset at the end. + let err = res.err().unwrap_or(Error::Closed); + this.reset_subscribe(msg.id, err).await.ok(); + + // We're all done, so clean up the abort handle. + this.subscribes.lock().unwrap().remove(&msg.id); + }); + + Ok(handle.abort_handle()) + } + + async fn run_subscribe(&self, id: VarInt, track: &mut track::Subscriber) -> Result<(), Error> { + // TODO add an Ok method to track::Publisher so we can send SUBSCRIBE_OK + + while let Some(mut segment) = track.next_segment().await? { + // TODO only clone the fields we need + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.run_segment(id, &mut segment).await { + log::warn!("failed to serve segment: {:?}", err) + } + }); + } + + Ok(()) + } + + async fn run_segment(&self, id: VarInt, segment: &mut segment::Subscriber) -> Result<(), Error> { + let object = message::Object { + track: id, + sequence: segment.sequence, + priority: segment.priority, + expires: segment.expires, + }; + + log::debug!("serving object: {:?}", object); + + let mut stream = self.webtransport.open_uni().await.map_err(|_e| Error::Write)?; + + stream.set_priority(object.priority).ok(); + + // TODO better handle the error. + object.encode(&mut stream).await.map_err(|_e| Error::Write)?; + + while let Some(data) = segment.read_chunk().await? { + stream.write_chunk(data).await.map_err(|_e| Error::Write)?; + } + + Ok(()) + } + + async fn recv_subscribe_stop(&mut self, msg: &message::SubscribeStop) -> Result<(), Error> { + let abort = self.subscribes.lock().unwrap().remove(&msg.id).ok_or(Error::NotFound)?; + abort.abort(); + + self.reset_subscribe(msg.id, Error::Stop).await + } +} diff --git a/moq-transport/src/session/server.rs b/moq-transport/src/session/server.rs new file mode 100644 index 0000000..b571209 --- /dev/null +++ b/moq-transport/src/session/server.rs @@ -0,0 +1,100 @@ +use super::{Publisher, Subscriber}; +use crate::{model::broadcast, setup}; + +use webtransport_quinn::{RecvStream, SendStream, Session}; + +use anyhow::Context; + +/// An endpoint that accepts connections, publishing and/or consuming live streams. +pub struct Server {} + +impl Server { + /// Accept an established Webtransport session, performing the MoQ handshake. + /// + /// This returns a [Request] half-way through the handshake that allows the application to accept or deny the session. + pub async fn accept(session: Session) -> anyhow::Result { + let mut control = session.accept_bi().await.context("failed to accept bidi stream")?; + + let client = setup::Client::decode(&mut control.1) + .await + .context("failed to read CLIENT SETUP")?; + + client + .versions + .iter() + .find(|version| **version == setup::Version::KIXEL_00) + .context("no supported versions")?; + + Ok(Request { + session, + client, + control, + }) + } +} + +/// A partially complete MoQ Transport handshake. +pub struct Request { + session: Session, + client: setup::Client, + control: (SendStream, RecvStream), +} + +impl Request { + /// Accept the session as a publisher, using the provided broadcast to serve subscriptions. + pub async fn publisher(mut self, source: broadcast::Subscriber) -> anyhow::Result { + self.send_setup(setup::Role::Publisher).await?; + + let publisher = Publisher::new(self.session, self.control, source); + Ok(publisher) + } + + /// Accept the session as a subscriber only. + pub async fn subscriber(mut self, source: broadcast::Publisher) -> anyhow::Result { + self.send_setup(setup::Role::Subscriber).await?; + + let subscriber = Subscriber::new(self.session, self.control, source); + Ok(subscriber) + } + + // TODO Accept the session and perform both roles. + /* + pub async fn accept(self) -> anyhow::Result<(Publisher, Subscriber)> { + self.ok(setup::Role::Both).await + } + */ + + async fn send_setup(&mut self, role: setup::Role) -> anyhow::Result<()> { + let server = setup::Server { + role, + version: setup::Version::KIXEL_00, + }; + + // We need to sure we support the opposite of the client's role. + // ex. if the client is a publisher, we must be a subscriber ONLY. + if !self.client.role.is_compatible(server.role) { + anyhow::bail!( + "incompatible roles: client={:?} server={:?}", + self.client.role, + server.role + ); + } + + server + .encode(&mut self.control.0) + .await + .context("failed to send setup server")?; + + Ok(()) + } + + /// Reject the request, closing the Webtransport session. + pub fn reject(self, code: u32) { + self.session.close(code, b"") + } + + /// The role advertised by the client. + pub fn role(&self) -> setup::Role { + self.client.role + } +} diff --git a/moq-transport/src/session/subscriber.rs b/moq-transport/src/session/subscriber.rs new file mode 100644 index 0000000..f89f400 --- /dev/null +++ b/moq-transport/src/session/subscriber.rs @@ -0,0 +1,152 @@ +use webtransport_quinn::{RecvStream, SendStream, Session}; + +use std::{ + collections::HashMap, + sync::{atomic, Arc, Mutex}, +}; + +use crate::{ + message, + message::Message, + model::{broadcast, segment, track}, + Error, VarInt, +}; + +use super::Control; + +/// Receives broadcasts over the network, automatically handling subscriptions and caching. +// TODO Clone specific fields when a task actually needs it. +#[derive(Clone, Debug)] +pub struct Subscriber { + // The webtransport session. + webtransport: Session, + + // The list of active subscriptions, each guarded by an mutex. + subscribes: Arc>>, + + // The sequence number for the next subscription. + next: Arc, + + // A channel for sending messages. + control: Control, + + // All unknown subscribes comes here. + source: broadcast::Publisher, +} + +impl Subscriber { + pub(crate) fn new(webtransport: Session, control: (SendStream, RecvStream), source: broadcast::Publisher) -> Self { + let control = Control::new(control.0, control.1); + + Self { + webtransport, + subscribes: Default::default(), + next: Default::default(), + control, + source, + } + } + + pub async fn run(self) -> Result<(), Error> { + let inbound = self.clone().run_inbound(); + let streams = self.clone().run_streams(); + let source = self.clone().run_source(); + + // Return the first error. + tokio::select! { + res = inbound => res, + res = streams => res, + res = source => res, + } + } + + async fn run_inbound(mut self) -> Result<(), Error> { + loop { + let msg = self.control.recv().await.map_err(|_e| Error::Read)?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg).await { + log::warn!("message error: {:?} {:?}", err, msg); + } + } + } + + async fn recv_message(&mut self, msg: &Message) -> Result<(), Error> { + match msg { + Message::Announce(_) => Ok(()), // don't care + Message::AnnounceReset(_) => Ok(()), // also don't care + Message::SubscribeOk(_) => Ok(()), // guess what, don't care + Message::SubscribeReset(msg) => self.recv_subscribe_reset(msg).await, + Message::GoAway(_msg) => unimplemented!("GOAWAY"), + _ => Err(Error::Role(msg.id())), + } + } + + async fn recv_subscribe_reset(&mut self, msg: &message::SubscribeReset) -> Result<(), Error> { + let err = Error::Reset(msg.code); + + let mut subscribes = self.subscribes.lock().unwrap(); + let subscribe = subscribes.remove(&msg.id).ok_or(Error::NotFound)?; + subscribe.close(err)?; + + Ok(()) + } + + async fn run_streams(self) -> Result<(), Error> { + loop { + // Accept all incoming unidirectional streams. + let stream = self.webtransport.accept_uni().await.map_err(|_| Error::Read)?; + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.run_stream(stream).await { + log::warn!("failed to receive stream: err={:?}", err); + } + }); + } + } + + async fn run_stream(self, mut stream: RecvStream) -> Result<(), Error> { + // Decode the object on the data stream. + let object = message::Object::decode(&mut stream).await.map_err(|_| Error::Read)?; + + log::debug!("received object: {:?}", object); + + // A new scope is needed because the async compiler is dumb + let mut publisher = { + let mut subscribes = self.subscribes.lock().unwrap(); + let track = subscribes.get_mut(&object.track).ok_or(Error::NotFound)?; + + track.create_segment(segment::Info { + sequence: object.sequence, + priority: object.priority, + expires: object.expires, + })? + }; + + while let Some(data) = stream.read_chunk(usize::MAX, true).await.map_err(|_| Error::Read)? { + publisher.write_chunk(data.bytes)?; + } + + Ok(()) + } + + async fn run_source(mut self) -> Result<(), Error> { + while let Some(track) = self.source.next_track().await? { + let name = track.name.clone(); + + let id = VarInt::from_u32(self.next.fetch_add(1, atomic::Ordering::SeqCst)); + self.subscribes.lock().unwrap().insert(id, track); + + let msg = message::Subscribe { + id, + namespace: "".to_string(), + name, + }; + + self.control.send(msg).await?; + } + + Ok(()) + } +} diff --git a/moq-transport/src/setup/client.rs b/moq-transport/src/setup/client.rs index 6648c66..220ff52 100644 --- a/moq-transport/src/setup/client.rs +++ b/moq-transport/src/setup/client.rs @@ -1,30 +1,27 @@ use super::{Role, Versions}; use crate::{ - coding::{decode_string, encode_string, DecodeError, EncodeError}, + coding::{DecodeError, EncodeError}, VarInt, }; -use webtransport_generic::{RecvStream, SendStream}; +use crate::coding::{AsyncRead, AsyncWrite}; -// Sent by the client to setup up the session. +/// Sent by the client to setup the session. +// NOTE: This is not a message type, but rather the control stream header. +// Proposal: https://github.com/moq-wg/moq-transport/issues/138 #[derive(Debug)] pub struct Client { - // NOTE: This is not a message type, but rather the control stream header. - // Proposal: https://github.com/moq-wg/moq-transport/issues/138 - - // The list of supported versions in preferred order. + /// The list of supported versions in preferred order. pub versions: Versions, - // Indicate if the client is a publisher, a subscriber, or both. + /// Indicate if the client is a publisher, a subscriber, or both. // Proposal: moq-wg/moq-transport#151 pub role: Role, - - // The path, non-empty ONLY when not using WebTransport. - pub path: String, } impl Client { - pub async fn decode(r: &mut R) -> Result { + /// Decode a client setup message. + pub async fn decode(r: &mut R) -> Result { let typ = VarInt::decode(r).await?; if typ.into_inner() != 1 { return Err(DecodeError::InvalidType(typ)); @@ -32,16 +29,15 @@ impl Client { let versions = Versions::decode(r).await?; let role = Role::decode(r).await?; - let path = decode_string(r).await?; - Ok(Self { versions, role, path }) + Ok(Self { versions, role }) } - pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + /// Encode a server setup message. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { VarInt::from_u32(1).encode(w).await?; self.versions.encode(w).await?; self.role.encode(w).await?; - encode_string(&self.path, w).await?; Ok(()) } diff --git a/moq-transport/src/setup/mod.rs b/moq-transport/src/setup/mod.rs index 3fc9ab7..e5c59c8 100644 --- a/moq-transport/src/setup/mod.rs +++ b/moq-transport/src/setup/mod.rs @@ -1,3 +1,9 @@ +//! Messages used for the MoQ Transport handshake. +//! +//! After establishing the WebTransport session, the client creates a bidirectional QUIC stream. +//! The client sends the [Client] message and the server responds with the [Server] message. +//! Both sides negotate the [Version] and [Role]. + mod client; mod role; mod server; diff --git a/moq-transport/src/setup/role.rs b/moq-transport/src/setup/role.rs index c8d6222..9620697 100644 --- a/moq-transport/src/setup/role.rs +++ b/moq-transport/src/setup/role.rs @@ -1,7 +1,8 @@ -use webtransport_generic::{RecvStream, SendStream}; +use crate::coding::{AsyncRead, AsyncWrite}; use crate::coding::{DecodeError, EncodeError, VarInt}; +/// Indicates the endpoint is a publisher, subscriber, or both. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Role { Publisher, @@ -10,6 +11,7 @@ pub enum Role { } impl Role { + /// Returns true if the role is publisher. pub fn is_publisher(&self) -> bool { match self { Self::Publisher | Self::Both => true, @@ -17,12 +19,18 @@ impl Role { } } + /// Returns true if the role is a subscriber. pub fn is_subscriber(&self) -> bool { match self { Self::Subscriber | Self::Both => true, Self::Publisher => false, } } + + /// Returns true if two endpoints are compatible. + pub fn is_compatible(&self, other: Role) -> bool { + self.is_publisher() == other.is_subscriber() && self.is_subscriber() == other.is_publisher() + } } impl From for VarInt { @@ -49,12 +57,14 @@ impl TryFrom for Role { } impl Role { - pub async fn decode(r: &mut R) -> Result { + /// Decode the role. + pub async fn decode(r: &mut R) -> Result { let v = VarInt::decode(r).await?; v.try_into() } - pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + /// Encode the role. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { VarInt::from(*self).encode(w).await } } diff --git a/moq-transport/src/setup/server.rs b/moq-transport/src/setup/server.rs index 85ebacc..819bb4c 100644 --- a/moq-transport/src/setup/server.rs +++ b/moq-transport/src/setup/server.rs @@ -4,23 +4,24 @@ use crate::{ VarInt, }; -use webtransport_generic::{RecvStream, SendStream}; +use crate::coding::{AsyncRead, AsyncWrite}; -// Sent by the server in response to a client. +/// Sent by the server in response to a client setup. // NOTE: This is not a message type, but rather the control stream header. // Proposal: https://github.com/moq-wg/moq-transport/issues/138 #[derive(Debug)] pub struct Server { - // The list of supported versions in preferred order. + /// The list of supported versions in preferred order. pub version: Version, - // param: 0x0: Indicate if the server is a publisher, a subscriber, or both. + /// Indicate if the server is a publisher, a subscriber, or both. // Proposal: moq-wg/moq-transport#151 pub role: Role, } impl Server { - pub async fn decode(r: &mut R) -> Result { + /// Decode the server setup. + pub async fn decode(r: &mut R) -> Result { let typ = VarInt::decode(r).await?; if typ.into_inner() != 2 { return Err(DecodeError::InvalidType(typ)); @@ -32,7 +33,8 @@ impl Server { Ok(Self { version, role }) } - pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + /// Encode the server setup. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { VarInt::from_u32(2).encode(w).await?; self.version.encode(w).await?; self.role.encode(w).await?; diff --git a/moq-transport/src/setup/version.rs b/moq-transport/src/setup/version.rs index 1fe75d4..6b2f143 100644 --- a/moq-transport/src/setup/version.rs +++ b/moq-transport/src/setup/version.rs @@ -1,14 +1,61 @@ use crate::coding::{DecodeError, EncodeError, VarInt}; -use webtransport_generic::{RecvStream, SendStream}; +use crate::coding::{AsyncRead, AsyncWrite}; use std::ops::Deref; +/// A version number negotiated during the setup. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Version(pub VarInt); +pub struct Version(VarInt); impl Version { + /// pub const DRAFT_00: Version = Version(VarInt::from_u32(0xff00)); + + /// Fork of draft-ietf-moq-transport-00. + /// + /// Rough list of differences: + /// + /// # Messages + /// - Messages are sent over a control stream or a data stream. + /// - Data streams: each unidirectional stream contains a single OBJECT message. + /// - Control stream: a (client-initiated) bidirectional stream containing SETUP and then all other messages. + /// - Messages do not contain a length; unknown messages are fatal. + /// + /// # SETUP + /// - SETUP is split into SETUP_CLIENT and SETUP_SERVER with separate IDs. + /// - SETUP uses version `0xff00` for draft-00. + /// - SETUP no longer contains optional parameters; all are encoded in order and possibly zero. + /// - SETUP `role` indicates the role of the sender, not the role of the server. + /// - SETUP `path` field removed; use WebTransport for path. + /// + /// # SUBSCRIBE + /// - SUBSCRIBE `full_name` is split into separate `namespace` and `name` fields. + /// - SUBSCRIBE no longer contains optional parameters; all are encoded in order and possibly zero. + /// - SUBSCRIBE no longer contains the `auth` parameter; use WebTransport for auth. + /// - SUBSCRIBE no longer contains the `group` parameter; concept no longer exists. + /// - SUBSCRIBE contains the `id` instead of SUBSCRIBE_OK. + /// - SUBSCRIBE_OK and SUBSCRIBE_ERROR reference the subscription `id` the instead of the track `full_name`. + /// - SUBSCRIBE_ERROR was renamed to SUBSCRIBE_RESET, sent by publisher to terminate a SUBSCRIBE. + /// - SUBSCRIBE_STOP was added, sent by the subscriber to terminate a SUBSCRIBE. + /// - SUBSCRIBE_OK no longer has `expires`. + /// + /// # ANNOUNCE + /// - ANNOUNCE no longer contains optional parameters; all are encoded in order and possibly zero. + /// - ANNOUNCE no longer contains the `auth` field; use WebTransport for auth. + /// - ANNOUNCE_ERROR was renamed to ANNOUNCE_RESET, sent by publisher to terminate an ANNOUNCE. + /// - ANNOUNCE_STOP was added, sent by the subscriber to terminate an ANNOUNCE. + /// + /// # OBJECT + /// - OBJECT uses a dedicated QUIC stream. + /// - OBJECT has no size and continues until stream FIN. + /// - OBJECT `priority` is a i32 instead of a varint. (for practical reasons) + /// - OBJECT `expires` was added, a varint in seconds. + /// - OBJECT `group` was removed. + /// + /// # GROUP + /// - GROUP concept was removed, replaced with OBJECT as a QUIC stream. + pub const KIXEL_00: Version = Version(VarInt::from_u32(0xbad00)); } impl From for Version { @@ -24,22 +71,26 @@ impl From for VarInt { } impl Version { - pub async fn decode(r: &mut R) -> Result { + /// Decode the version number. + pub async fn decode(r: &mut R) -> Result { let v = VarInt::decode(r).await?; Ok(Self(v)) } - pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + /// Encode the version number. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { self.0.encode(w).await?; Ok(()) } } +/// A list of versions in arbitrary order. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Versions(pub Vec); +pub struct Versions(Vec); impl Versions { - pub async fn decode(r: &mut R) -> Result { + /// Decode the version list. + pub async fn decode(r: &mut R) -> Result { let count = VarInt::decode(r).await?.into_inner(); let mut vs = Vec::new(); @@ -51,7 +102,8 @@ impl Versions { Ok(Self(vs)) } - pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + /// Encode the version list. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { let size: VarInt = self.0.len().try_into()?; size.encode(w).await?; diff --git a/moq-warp/Cargo.toml b/moq-warp/Cargo.toml deleted file mode 100644 index 0e1b02b..0000000 --- a/moq-warp/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "moq-warp" -description = "Media over QUIC" -authors = ["Luke Curley"] -repository = "https://github.com/kixelated/moq-rs" -license = "MIT OR Apache-2.0" - -version = "0.1.0" -edition = "2021" - -keywords = ["quic", "http3", "webtransport", "media", "live"] -categories = ["multimedia", "network-programming", "web-programming"] - - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -moq-transport = { path = "../moq-transport" } -webtransport-generic = "0.5" - -tokio = "1.27" -anyhow = "1.0.70" -log = "0.4" # TODO remove -bytes = "1.4" diff --git a/moq-warp/src/lib.rs b/moq-warp/src/lib.rs deleted file mode 100644 index 3a31bdd..0000000 --- a/moq-warp/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod model; -pub mod relay; diff --git a/moq-warp/src/model/broadcast.rs b/moq-warp/src/model/broadcast.rs deleted file mode 100644 index 3bd0dee..0000000 --- a/moq-warp/src/model/broadcast.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{error, fmt}; - -use moq_transport::VarInt; - -// TODO generialize broker::Broadcasts and source::Source into this module. - -/* -pub struct Publisher { - pub namespace: String, - - pub tracks: watch::Publisher, -} - -impl Publisher { - pub fn new(namespace: &str) -> Self { - Self { - namespace: namespace.to_string(), - tracks: watch::Publisher::new(), - } - } - - pub fn subscribe(&self) -> Subscriber { - Subscriber { - namespace: self.namespace.clone(), - tracks: self.tracks.subscribe(), - } - } -} - -#[derive(Clone)] -pub struct Subscriber { - pub namespace: String, - - pub tracks: watch::Subscriber, -} -*/ - -#[derive(Clone)] -pub struct Error { - pub code: VarInt, - pub reason: String, -} - -impl error::Error for Error {} - -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !self.reason.is_empty() { - write!(f, "broadcast error ({}): {}", self.code, self.reason) - } else { - write!(f, "broadcast error ({})", self.code) - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !self.reason.is_empty() { - write!(f, "broadcast error ({}): {}", self.code, self.reason) - } else { - write!(f, "broadcast error ({})", self.code) - } - } -} diff --git a/moq-warp/src/model/fragment.rs b/moq-warp/src/model/fragment.rs deleted file mode 100644 index c6a6aa4..0000000 --- a/moq-warp/src/model/fragment.rs +++ /dev/null @@ -1,5 +0,0 @@ -use super::watch; -use bytes::Bytes; - -pub type Publisher = watch::Publisher; -pub type Subscriber = watch::Subscriber; diff --git a/moq-warp/src/model/mod.rs b/moq-warp/src/model/mod.rs deleted file mode 100644 index 55e5425..0000000 --- a/moq-warp/src/model/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod broadcast; -pub mod fragment; -pub mod segment; -pub mod track; -pub mod watch; diff --git a/moq-warp/src/model/segment.rs b/moq-warp/src/model/segment.rs deleted file mode 100644 index 5726e39..0000000 --- a/moq-warp/src/model/segment.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::watch; - -use bytes::Bytes; -use moq_transport::VarInt; -use std::ops::Deref; -use std::sync::Arc; -use std::time; - -#[derive(Clone, 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 send_order: i32, - - // The time at which the segment expires for cache purposes. - pub expires: Option, -} - -pub struct Publisher { - pub info: Arc, - - // A list of fragments that make up the segment. - pub fragments: watch::Publisher, -} - -impl Publisher { - pub fn new(info: Info) -> Self { - Self { - info: Arc::new(info), - fragments: watch::Publisher::new(), - } - } - - pub fn subscribe(&self) -> Subscriber { - Subscriber { - info: self.info.clone(), - fragments: self.fragments.subscribe(), - } - } -} - -impl Deref for Publisher { - type Target = Info; - - fn deref(&self) -> &Self::Target { - &self.info - } -} - -#[derive(Clone, Debug)] -pub struct Subscriber { - pub info: Arc, - - // A list of fragments that make up the segment. - pub fragments: watch::Subscriber, -} - -impl Deref for Subscriber { - type Target = Info; - - fn deref(&self) -> &Self::Target { - &self.info - } -} diff --git a/moq-warp/src/model/track.rs b/moq-warp/src/model/track.rs deleted file mode 100644 index 0704c72..0000000 --- a/moq-warp/src/model/track.rs +++ /dev/null @@ -1,101 +0,0 @@ -use super::{segment, watch}; -use std::{error, fmt, time}; - -use moq_transport::VarInt; - -pub struct Publisher { - pub name: String, - - segments: watch::Publisher>, -} - -impl Publisher { - pub fn new(name: &str) -> Publisher { - Self { - name: name.to_string(), - segments: watch::Publisher::new(), - } - } - - pub fn push_segment(&mut self, segment: segment::Subscriber) { - self.segments.push(Ok(segment)) - } - - pub fn drain_segments(&mut self, before: time::Instant) { - self.segments.drain(|segment| { - if let Ok(segment) = segment { - if let Some(expires) = segment.expires { - return expires < before; - } - } - - false - }) - } - - pub fn close(mut self, err: Error) { - self.segments.push(Err(err)) - } - - pub fn subscribe(&self) -> Subscriber { - Subscriber { - name: self.name.clone(), - segments: self.segments.subscribe(), - } - } -} - -impl fmt::Debug for Publisher { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "track publisher: {:?}", self.name) - } -} - -#[derive(Clone, Debug)] -pub struct Subscriber { - pub name: String, - - // A list of segments, which are independently decodable. - segments: watch::Subscriber>, -} - -impl Subscriber { - pub async fn next_segment(&mut self) -> Result { - let res = self.segments.next().await; - match res { - None => Err(Error { - code: VarInt::from_u32(0), - reason: String::from("closed"), - }), - Some(res) => res, - } - } -} - -#[derive(Clone)] -pub struct Error { - pub code: VarInt, - pub reason: String, -} - -impl error::Error for Error {} - -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !self.reason.is_empty() { - write!(f, "track error ({}): {}", self.code, self.reason) - } else { - write!(f, "track error ({})", self.code) - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !self.reason.is_empty() { - write!(f, "track error ({}): {}", self.code, self.reason) - } else { - write!(f, "track error ({})", self.code) - } - } -} diff --git a/moq-warp/src/model/watch.rs b/moq-warp/src/model/watch.rs deleted file mode 100644 index 3ae16df..0000000 --- a/moq-warp/src/model/watch.rs +++ /dev/null @@ -1,135 +0,0 @@ -use core::fmt; -use std::collections::VecDeque; -use tokio::sync::watch; - -#[derive(Default)] -struct State { - queue: VecDeque, - drained: usize, -} - -impl fmt::Debug for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "State<{}> ( queue.len(): {}, drained: {} )", - std::any::type_name::(), - &self.queue.len(), - &self.drained - ) - } -} - -impl State { - fn new() -> Self { - Self { - queue: VecDeque::new(), - drained: 0, - } - } - - // Add a new element to the end of the queue. - fn push(&mut self, t: T) { - self.queue.push_back(t) - } - - // Remove elements from the head of the queue if they match the conditional. - fn drain(&mut self, f: F) -> usize - where - F: Fn(&T) -> bool, - { - let prior = self.drained; - - while let Some(first) = self.queue.front() { - if !f(first) { - break; - } - - self.queue.pop_front(); - self.drained += 1; - } - - self.drained - prior - } -} - -pub struct Publisher { - sender: watch::Sender>, -} - -impl Publisher { - pub fn new() -> Self { - let state = State::new(); - let (sender, _) = watch::channel(state); - Self { sender } - } - - // Push a new element to the end of the queue. - pub fn push(&mut self, value: T) { - self.sender.send_modify(|state| state.push(value)); - } - - // Remove any elements from the front of the queue that match the condition. - pub fn drain(&mut self, f: F) - where - F: Fn(&T) -> bool, - { - // Use send_if_modified to never notify with the updated state. - self.sender.send_if_modified(|state| { - state.drain(f); - false - }); - } - - // Subscribe for all NEW updates. - pub fn subscribe(&self) -> Subscriber { - let index = self.sender.borrow().queue.len(); - - Subscriber { - state: self.sender.subscribe(), - index, - } - } -} - -impl Default for Publisher { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Debug)] -pub struct Subscriber { - state: watch::Receiver>, - index: usize, -} - -impl Subscriber { - pub async fn next(&mut self) -> Option { - // Wait until the queue has a new element or if it's closed. - let state = self - .state - .wait_for(|state| self.index < state.drained + state.queue.len()) - .await; - - let state = match state { - Ok(state) => state, - Err(_) => return None, // publisher was dropped - }; - - // If our index is smaller than drained, skip past those elements we missed. - let index = self.index.saturating_sub(state.drained); - - if index < state.queue.len() { - // Clone the next element in the queue. - let element = state.queue[index].clone(); - - // Increment our index, relative to drained so we can skip ahead if needed. - self.index = index + state.drained + 1; - - Some(element) - } else { - unreachable!("impossible subscriber state") - } - } -} diff --git a/moq-warp/src/relay/broker.rs b/moq-warp/src/relay/broker.rs deleted file mode 100644 index 912c1e9..0000000 --- a/moq-warp/src/relay/broker.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::model::{broadcast, track, watch}; -use crate::relay::contribute; - -use std::collections::hash_map::HashMap; -use std::sync::{Arc, Mutex}; - -use anyhow::Context; - -#[derive(Clone, Default)] -pub struct Broker { - // Operate on the inner struct so we can share/clone the outer struct. - inner: Arc>, -} - -#[derive(Default)] -struct BrokerInner { - // TODO Automatically reclaim dropped sources. - lookup: HashMap>, - updates: watch::Publisher, -} - -#[derive(Clone)] -pub enum BrokerUpdate { - // Broadcast was announced - Insert(String), // TODO include source? - - // Broadcast was unannounced - Remove(String, broadcast::Error), -} - -impl Broker { - pub fn new() -> Self { - Default::default() - } - - // Return the list of available broadcasts, and a subscriber that will return updates (add/remove). - pub fn available(&self) -> (Vec, watch::Subscriber) { - // Grab the lock. - let this = self.inner.lock().unwrap(); - - // Get the list of all available tracks. - let keys = this.lookup.keys().cloned().collect(); - - // Get a subscriber that will return future updates. - let updates = this.updates.subscribe(); - - (keys, updates) - } - - pub fn announce(&self, namespace: &str, source: Arc) -> anyhow::Result<()> { - let mut this = self.inner.lock().unwrap(); - - if let Some(_existing) = this.lookup.get(namespace) { - anyhow::bail!("namespace already registered"); - } - - this.lookup.insert(namespace.to_string(), source); - this.updates.push(BrokerUpdate::Insert(namespace.to_string())); - - Ok(()) - } - - pub fn unannounce(&self, namespace: &str, error: broadcast::Error) -> anyhow::Result<()> { - let mut this = self.inner.lock().unwrap(); - - this.lookup.remove(namespace).context("namespace was not published")?; - this.updates.push(BrokerUpdate::Remove(namespace.to_string(), error)); - - Ok(()) - } - - pub fn subscribe(&self, namespace: &str, name: &str) -> Option { - let this = self.inner.lock().unwrap(); - this.lookup.get(namespace).and_then(|v| v.subscribe(name)) - } -} diff --git a/moq-warp/src/relay/contribute.rs b/moq-warp/src/relay/contribute.rs deleted file mode 100644 index c36eb0e..0000000 --- a/moq-warp/src/relay/contribute.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::time; - -use tokio::io::AsyncReadExt; -use tokio::sync::mpsc; -use tokio::task::JoinSet; // lock across await boundaries - -use moq_transport::message::{Announce, AnnounceError, AnnounceOk, Subscribe, SubscribeError, SubscribeOk}; -use moq_transport::{object, Object, VarInt}; -use webtransport_generic::Session as WTSession; - -use bytes::BytesMut; - -use anyhow::Context; - -use crate::model::{broadcast, segment, track}; -use crate::relay::{ - message::{Component, Contribute}, - Broker, -}; - -// TODO experiment with making this Clone, so every task can have its own copy. -pub struct Session { - // Used to receive objects. - objects: object::Receiver, - - // Used to send and receive control messages. - control: Component, - - // Globally announced namespaces, which we can add ourselves to. - broker: Broker, - - // The names of active broadcasts being produced. - broadcasts: HashMap>, - - // Active tracks being produced by this session. - publishers: Publishers, - - // Tasks we are currently serving. - run_segments: JoinSet>, // receiving objects -} - -impl Session { - pub fn new(objects: object::Receiver, control: Component, broker: Broker) -> Self { - Self { - objects, - control, - broker, - broadcasts: HashMap::new(), - publishers: Publishers::new(), - run_segments: JoinSet::new(), - } - } - - pub async fn run(mut self) -> anyhow::Result<()> { - loop { - tokio::select! { - res = self.run_segments.join_next(), if !self.run_segments.is_empty() => { - let res = res.expect("no tasks").expect("task aborted"); - if let Err(err) = res { - log::warn!("failed to produce segment: {:?}", err); - } - }, - object = self.objects.recv() => { - let (object, stream) = object.context("failed to receive object")?; - let res = self.receive_object(object, stream).await; - if let Err(err) = res { - log::warn!("failed to receive object: {:?}", err); - } - }, - subscribe = self.publishers.incoming() => { - let msg = subscribe.context("failed to receive subscription")?; - self.control.send(msg).await?; - }, - msg = self.control.recv() => { - let msg = msg.context("failed to receive control message")?; - self.receive_message(msg).await?; - }, - } - } - } - - async fn receive_message(&mut self, msg: Contribute) -> anyhow::Result<()> { - match msg { - Contribute::Announce(msg) => self.receive_announce(msg).await, - Contribute::SubscribeOk(msg) => self.receive_subscribe_ok(msg), - Contribute::SubscribeError(msg) => self.receive_subscribe_error(msg), - } - } - - async fn receive_object(&mut self, obj: Object, stream: S::RecvStream) -> anyhow::Result<()> { - let track = obj.track; - - // Keep objects in memory for 10s - let expires = time::Instant::now() + time::Duration::from_secs(10); - - let segment = segment::Info { - sequence: obj.sequence, - send_order: obj.send_order, - expires: Some(expires), - }; - - let segment = segment::Publisher::new(segment); - - self.publishers - .push_segment(track, segment.subscribe()) - .context("failed to publish segment")?; - - // TODO implement a timeout - - self.run_segments - .spawn(async move { Self::run_segment(segment, stream).await }); - - Ok(()) - } - - async fn run_segment(mut segment: segment::Publisher, mut stream: S::RecvStream) -> anyhow::Result<()> { - let mut buf = BytesMut::new(); - - while stream.read_buf(&mut buf).await? > 0 { - // Split off the data we read into the buffer, freezing it so multiple threads can read simitaniously. - let data = buf.split().freeze(); - segment.fragments.push(data); - } - - Ok(()) - } - - async fn receive_announce(&mut self, msg: Announce) -> anyhow::Result<()> { - match self.receive_announce_inner(&msg).await { - Ok(()) => { - let msg = AnnounceOk { - track_namespace: msg.track_namespace, - }; - self.control.send(msg).await - } - Err(e) => { - let msg = AnnounceError { - track_namespace: msg.track_namespace, - code: VarInt::from_u32(1), - reason: e.to_string(), - }; - self.control.send(msg).await - } - } - } - - async fn receive_announce_inner(&mut self, msg: &Announce) -> anyhow::Result<()> { - // Create a broadcast and announce it. - // We don't actually start producing the broadcast until we receive a subscription. - let broadcast = Arc::new(Broadcast::new(&msg.track_namespace, &self.publishers)); - - self.broker.announce(&msg.track_namespace, broadcast.clone())?; - self.broadcasts.insert(msg.track_namespace.clone(), broadcast); - - Ok(()) - } - - fn receive_subscribe_ok(&mut self, _msg: SubscribeOk) -> anyhow::Result<()> { - // TODO make sure this is for a track we are subscribed to - Ok(()) - } - - fn receive_subscribe_error(&mut self, msg: SubscribeError) -> anyhow::Result<()> { - let error = track::Error { - code: msg.code, - reason: msg.reason, - }; - - // Stop producing the track. - self.publishers - .close(msg.track_id, error) - .context("failed to close track")?; - - Ok(()) - } -} - -impl Drop for Session { - fn drop(&mut self) { - // Unannounce all broadcasts we have announced. - // TODO make this automatic so we can't screw up? - // TOOD Implement UNANNOUNCE so we can return good errors. - for broadcast in self.broadcasts.keys() { - let error = broadcast::Error { - code: VarInt::from_u32(1), - reason: "connection closed".to_string(), - }; - - self.broker.unannounce(broadcast, error).unwrap(); - } - } -} - -// A list of subscriptions for a broadcast. -#[derive(Clone)] -pub struct Broadcast { - // Our namespace - namespace: String, - - // A lookup from name to a subscription (duplicate subscribers) - subscriptions: Arc>>, - - // Issue a SUBSCRIBE message for a new subscription (new subscriber) - queue: mpsc::UnboundedSender<(String, track::Publisher)>, -} - -impl Broadcast { - pub fn new(namespace: &str, publishers: &Publishers) -> Self { - Self { - namespace: namespace.to_string(), - subscriptions: Default::default(), - queue: publishers.sender.clone(), - } - } - - pub fn subscribe(&self, name: &str) -> Option { - let mut subscriptions = self.subscriptions.lock().unwrap(); - - // Check if there's an existing subscription. - if let Some(subscriber) = subscriptions.get(name).cloned() { - return Some(subscriber); - } - - // Otherwise, make a new track and tell the publisher to fufill it. - let track = track::Publisher::new(name); - let subscriber = track.subscribe(); - - // Save the subscriber for duplication. - subscriptions.insert(name.to_string(), subscriber.clone()); - - // Send the publisher to another thread to actually subscribe. - self.queue.send((self.namespace.clone(), track)).unwrap(); - - // Return the subscriber we created. - Some(subscriber) - } -} - -pub struct Publishers { - // A lookup from subscription ID to a track being produced, or none if it's been closed. - tracks: HashMap>, - - // The next subscription ID - next: u64, - - // A queue of subscriptions that we need to fulfill - receiver: mpsc::UnboundedReceiver<(String, track::Publisher)>, - - // A clonable queue, so other threads can issue subscriptions. - sender: mpsc::UnboundedSender<(String, track::Publisher)>, -} - -impl Default for Publishers { - fn default() -> Self { - let (sender, receiver) = mpsc::unbounded_channel(); - - Self { - tracks: Default::default(), - next: 0, - sender, - receiver, - } - } -} - -impl Publishers { - pub fn new() -> Self { - Self::default() - } - - pub fn push_segment(&mut self, id: VarInt, segment: segment::Subscriber) -> anyhow::Result<()> { - let track = self.tracks.get_mut(&id).context("no track with that ID")?; - let track = track.as_mut().context("track closed")?; // TODO don't make fatal - - track.push_segment(segment); - track.drain_segments(time::Instant::now()); - - Ok(()) - } - - pub fn close(&mut self, id: VarInt, err: track::Error) -> anyhow::Result<()> { - let track = self.tracks.get_mut(&id).context("no track with that ID")?; - let track = track.take().context("track closed")?; - track.close(err); - - Ok(()) - } - - // Returns the next subscribe message we need to issue. - pub async fn incoming(&mut self) -> anyhow::Result { - let (namespace, track) = self.receiver.recv().await.context("no more subscriptions")?; - - let id = VarInt::try_from(self.next)?; - self.next += 1; - - let msg = Subscribe { - track_id: id, - track_namespace: namespace, - track_name: track.name.clone(), - }; - - self.tracks.insert(id, Some(track)); - - Ok(msg) - } -} diff --git a/moq-warp/src/relay/distribute.rs b/moq-warp/src/relay/distribute.rs deleted file mode 100644 index f1c275a..0000000 --- a/moq-warp/src/relay/distribute.rs +++ /dev/null @@ -1,205 +0,0 @@ -use anyhow::Context; - -use tokio::io::AsyncWriteExt; -use tokio::task::JoinSet; // allows locking across await - -use moq_transport::message::{Announce, AnnounceError, AnnounceOk, Subscribe, SubscribeError, SubscribeOk}; -use moq_transport::{object, Object, VarInt}; -use webtransport_generic::Session as WTSession; - -use crate::model::{segment, track}; -use crate::relay::{ - message::{Component, Distribute}, - Broker, BrokerUpdate, -}; - -pub struct Session { - // Objects are sent to the client - objects: object::Sender, - - // Used to send and receive control messages. - control: Component, - - // Globally announced namespaces, which can be subscribed to. - broker: Broker, - - // A list of tasks that are currently running. - run_subscribes: JoinSet, // run subscriptions, sending the returned error if they fail -} - -impl Session { - pub fn new(objects: object::Sender, control: Component, broker: Broker) -> Self { - Self { - objects, - control, - broker, - run_subscribes: JoinSet::new(), - } - } - - pub async fn run(mut self) -> anyhow::Result<()> { - // Announce all available tracks and get a stream of updates. - let (available, mut updates) = self.broker.available(); - for namespace in available { - self.on_available(BrokerUpdate::Insert(namespace)).await?; - } - - loop { - tokio::select! { - res = self.run_subscribes.join_next(), if !self.run_subscribes.is_empty() => { - let res = res.expect("no tasks").expect("task aborted"); - self.control.send(res).await?; - }, - delta = updates.next() => { - let delta = delta.expect("no more broadcasts"); - self.on_available(delta).await?; - }, - msg = self.control.recv() => { - let msg = msg.context("failed to receive control message")?; - self.receive_message(msg).await?; - }, - } - } - } - - async fn receive_message(&mut self, msg: Distribute) -> anyhow::Result<()> { - match msg { - Distribute::AnnounceOk(msg) => self.receive_announce_ok(msg), - Distribute::AnnounceError(msg) => self.receive_announce_error(msg), - Distribute::Subscribe(msg) => self.receive_subscribe(msg).await, - } - } - - fn receive_announce_ok(&mut self, _msg: AnnounceOk) -> anyhow::Result<()> { - // TODO make sure we sent this announce - Ok(()) - } - - fn receive_announce_error(&mut self, msg: AnnounceError) -> anyhow::Result<()> { - // TODO make sure we sent this announce - // TODO remove this from the list of subscribable broadcasts. - log::warn!("received error {:?}", msg); - Ok(()) - } - - async fn receive_subscribe(&mut self, msg: Subscribe) -> anyhow::Result<()> { - match self.receive_subscribe_inner(&msg).await { - Ok(()) => { - self.control - .send(SubscribeOk { - track_id: msg.track_id, - expires: None, - }) - .await - } - Err(e) => { - self.control - .send(SubscribeError { - track_id: msg.track_id, - code: VarInt::from_u32(1), - reason: e.to_string(), - }) - .await - } - } - } - - async fn receive_subscribe_inner(&mut self, msg: &Subscribe) -> anyhow::Result<()> { - let track = self - .broker - .subscribe(&msg.track_namespace, &msg.track_name) - .context("could not find broadcast")?; - - // TODO can we just clone self? - let objects = self.objects.clone(); - let track_id = msg.track_id; - - self.run_subscribes - .spawn(async move { Self::run_subscribe(objects, track_id, track).await }); - - Ok(()) - } - - async fn run_subscribe( - objects: object::Sender, - track_id: VarInt, - mut track: track::Subscriber, - ) -> SubscribeError { - let mut tasks = JoinSet::new(); - let mut result = None; - - loop { - tokio::select! { - // Accept new segments added to the track. - segment = track.next_segment(), if result.is_none() => { - match segment { - Ok(segment) => { - let objects = objects.clone(); - tasks.spawn(async move { Self::serve_group(objects, track_id, segment).await }); - }, - Err(e) => { - result = Some(SubscribeError { - track_id, - code: e.code, - reason: e.reason, - }) - }, - } - }, - // Poll any pending segments until they exit. - res = tasks.join_next(), if !tasks.is_empty() => { - let res = res.expect("no tasks").expect("task aborted"); - if let Err(err) = res { - log::error!("failed to serve segment: {:?}", err); - } - }, - else => return result.unwrap() - } - } - } - - async fn serve_group( - mut objects: object::Sender, - track_id: VarInt, - mut segment: segment::Subscriber, - ) -> anyhow::Result<()> { - 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, - }; - - 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?; - } - - // NOTE: stream is automatically closed when dropped - - Ok(()) - } - - async fn on_available(&mut self, delta: BrokerUpdate) -> anyhow::Result<()> { - match delta { - BrokerUpdate::Insert(name) => { - self.control - .send(Announce { - track_namespace: name.clone(), - }) - .await - } - BrokerUpdate::Remove(name, error) => { - self.control - .send(AnnounceError { - track_namespace: name, - code: error.code, - reason: error.reason, - }) - .await - } - } - } -} diff --git a/moq-warp/src/relay/message.rs b/moq-warp/src/relay/message.rs deleted file mode 100644 index 04c2f9c..0000000 --- a/moq-warp/src/relay/message.rs +++ /dev/null @@ -1,127 +0,0 @@ -use tokio::sync::mpsc; - -use moq_transport::message::{ - self, Announce, AnnounceError, AnnounceOk, Message, Subscribe, SubscribeError, SubscribeOk, -}; -use webtransport_generic::Session; - -pub struct Main { - send_control: message::Sender, - recv_control: message::Receiver, - - outgoing: mpsc::Receiver, - - contribute: mpsc::Sender, - distribute: mpsc::Sender, -} - -impl Main { - pub async fn run(mut self) -> anyhow::Result<()> { - loop { - tokio::select! { - Some(msg) = self.outgoing.recv() => self.send_control.send(msg).await?, - Ok(msg) = self.recv_control.recv() => self.handle(msg).await?, - } - } - } - - pub async fn handle(&mut self, msg: Message) -> anyhow::Result<()> { - match msg.try_into() { - Ok(msg) => self.contribute.send(msg).await?, - Err(msg) => match msg.try_into() { - Ok(msg) => self.distribute.send(msg).await?, - Err(msg) => anyhow::bail!("unsupported control message: {:?}", msg), - }, - } - - Ok(()) - } -} - -pub struct Component { - incoming: mpsc::Receiver, - outgoing: mpsc::Sender, -} - -impl Component { - pub async fn send>(&mut self, msg: M) -> anyhow::Result<()> { - self.outgoing.send(msg.into()).await?; - Ok(()) - } - - pub async fn recv(&mut self) -> Option { - self.incoming.recv().await - } -} - -// Splits a control stream into two components, based on if it's a message for contribution or distribution. -pub fn split( - send_control: message::Sender, - recv_control: message::Receiver, -) -> (Main, Component, Component) { - let (outgoing_tx, outgoing_rx) = mpsc::channel(1); - let (contribute_tx, contribute_rx) = mpsc::channel(1); - let (distribute_tx, distribute_rx) = mpsc::channel(1); - - let control = Main { - send_control, - recv_control, - outgoing: outgoing_rx, - contribute: contribute_tx, - distribute: distribute_tx, - }; - - let contribute = Component { - incoming: contribute_rx, - outgoing: outgoing_tx.clone(), - }; - - let distribute = Component { - incoming: distribute_rx, - outgoing: outgoing_tx, - }; - - (control, contribute, distribute) -} - -// Messages we expect to receive from the client for contribution. -#[derive(Debug)] -pub enum Contribute { - Announce(Announce), - SubscribeOk(SubscribeOk), - SubscribeError(SubscribeError), -} - -impl TryFrom for Contribute { - type Error = Message; - - fn try_from(msg: Message) -> Result { - match msg { - Message::Announce(msg) => Ok(Self::Announce(msg)), - Message::SubscribeOk(msg) => Ok(Self::SubscribeOk(msg)), - Message::SubscribeError(msg) => Ok(Self::SubscribeError(msg)), - _ => Err(msg), - } - } -} - -// Messages we expect to receive from the client for distribution. -#[derive(Debug)] -pub enum Distribute { - AnnounceOk(AnnounceOk), - AnnounceError(AnnounceError), - Subscribe(Subscribe), -} - -impl TryFrom for Distribute { - type Error = Message; - - fn try_from(value: Message) -> Result { - match value { - Message::AnnounceOk(msg) => Ok(Self::AnnounceOk(msg)), - Message::AnnounceError(msg) => Ok(Self::AnnounceError(msg)), - Message::Subscribe(msg) => Ok(Self::Subscribe(msg)), - _ => Err(value), - } - } -} diff --git a/moq-warp/src/relay/mod.rs b/moq-warp/src/relay/mod.rs deleted file mode 100644 index 6dcc217..0000000 --- a/moq-warp/src/relay/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod broker; -mod contribute; -mod distribute; -mod message; -mod session; - -pub use broker::*; -pub use session::*; diff --git a/moq-warp/src/relay/session.rs b/moq-warp/src/relay/session.rs deleted file mode 100644 index e9ae7f0..0000000 --- a/moq-warp/src/relay/session.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::relay::{contribute, distribute, message, Broker}; - -use webtransport_generic::Session as WTSession; - -pub struct Session { - // Split logic into contribution/distribution to reduce the problem space. - contribute: contribute::Session, - distribute: distribute::Session, - - // Used to receive control messages and forward to contribute/distribute. - control: message::Main, -} - -impl Session { - pub fn new(session: moq_transport::Session, broker: Broker) -> Self { - let (control, contribute, distribute) = message::split(session.send_control, session.recv_control); - - let contribute = contribute::Session::new(session.recv_objects, contribute, broker.clone()); - let distribute = distribute::Session::new(session.send_objects, distribute, broker); - - Self { - control, - contribute, - distribute, - } - } - - pub async fn run(self) -> anyhow::Result<()> { - let control = self.control.run(); - let contribute = self.contribute.run(); - let distribute = self.distribute.run(); - - tokio::try_join!(control, contribute, distribute)?; - - Ok(()) - } -}