From 0e5a06698f2ffffacd34da5c4b84d2d9ddef5dc2 Mon Sep 17 00:00:00 2001 From: Wanjohi <71614375+wanjohiryan@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:14:42 +0300 Subject: [PATCH 1/2] Add moq-rs video and audio transfer --- .gitignore | 1 + Cargo.lock | 1923 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 35 + src/main.rs | 3 + 4 files changed, 1962 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..84068ad --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1923 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +dependencies = [ + "backtrace", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e" +dependencies = [ + "concurrent-queue", + "event-listener 3.1.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d" +dependencies = [ + "async-lock 3.1.1", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.0.1", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel 1.9.0", + "async-executor", + "async-io", + "async-lock 2.8.0", + "blocking", + "futures-lite 1.13.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105" +dependencies = [ + "event-listener 3.1.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.1.0", + "async-lock 3.1.1", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.0.1", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clap_mangen" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3be86020147691e1d2ef58f75346a3d4d94807bfc473e377d52f09f0f7d77f7" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160" +dependencies = [ + "event-listener 3.1.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "four-cc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3958af68a31b1d1384d3f39b6aa33eb14b6009065b5ca305ddd9712a4237124f" + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.3", + "libc", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "moq-transport" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2fba4e2add2b2521abfe7dfdd87df1a55805663bfbaf56cb2774ece4183719" +dependencies = [ + "anyhow", + "bytes", + "indexmap", + "log", + "quinn", + "thiserror", + "tokio", + "webtransport-quinn 0.5.4", +] + +[[package]] +name = "mp4" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509348cba250e7b852a875100a2ddce7a36ee3abf881a681c756670c1774264d" +dependencies = [ + "byteorder", + "bytes", + "num-rational", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "mp4ra-rust" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9daf03b43bf3842962947c62ba40f411e46a58774c60838038f04a67d17626" +dependencies = [ + "four-cc", +] + +[[package]] +name = "mpeg4-audio-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a1fe2275b68991faded2c80aa4a33dba398b77d276038b8f50701a22e55918" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl-probe" +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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand", + "ring 0.16.20", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.5", + "tracing", + "windows-sys", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6381-codec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4395f46a67f0d57c57f6a5361f3a9a0c0183a19cab3998892ecdc003de6d8037" +dependencies = [ + "four-cc", + "mp4ra-rust", + "mpeg4-audio-const", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.21.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +dependencies = [ + "log", + "ring 0.17.5", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "warp" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "clap_mangen", + "env_logger", + "log", + "moq-transport", + "mp4", + "quinn", + "rfc6381-codec", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "webtransport-quinn 0.6.1", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webtransport-generic" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df712317d761312996f654739debeb3838eb02c6fd9146d9efdfd08a46674e45" +dependencies = [ + "bytes", + "tokio", +] + +[[package]] +name = "webtransport-proto" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54d41127a79f4d34112114b626f71d197c3ddf4fc82d56ccddc03a851bd0ea4f" +dependencies = [ + "bytes", + "http", + "thiserror", +] + +[[package]] +name = "webtransport-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeada5037d6302980ae2e0ab8d840e329c1697c612c6c077172de2b7631a276" +dependencies = [ + "bytes", + "http", + "thiserror", + "url", +] + +[[package]] +name = "webtransport-quinn" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7cccdcf10a2fb3a18ebd51fb8734e385624cb04fde38b239dbda0f1e40ba21" +dependencies = [ + "async-std", + "bytes", + "futures", + "http", + "quinn", + "quinn-proto", + "thiserror", + "tokio", + "webtransport-generic", + "webtransport-proto 0.5.4", +] + +[[package]] +name = "webtransport-quinn" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6536bd7382e3ecaeaf791fefbe8aa98d987eb5809ba7f1bd20617161d3a319" +dependencies = [ + "async-std", + "bytes", + "futures", + "http", + "log", + "quinn", + "quinn-proto", + "thiserror", + "tokio", + "url", + "webtransport-generic", + "webtransport-proto 0.6.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ebf65b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "warp" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +moq-transport = "0.2.0" + +# Copied from https://github.com/kixelated/moq-rs/moq-pub +quinn = "0.10" +webtransport-quinn = "0.6.1" +url = "2" +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" + +tokio = { version = "1", features = ["full"] } + +clap = { version = "4", features = ["derive"] } +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +mp4 = "0.13" +anyhow = { version = "1", features = ["backtrace"] } +serde_json = "1" +rfc6381-codec = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" + + +[build-dependencies] +clap = { version = "4", features = ["derive"] } +clap_mangen = "0.2" +url = "2" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From a092a84ec6a00e5cddacf786512c481e81737708 Mon Sep 17 00:00:00 2001 From: Wanjohi <71614375+wanjohiryan@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:10:47 +0300 Subject: [PATCH 2/2] This works --- .gitmodules | 3 + .vscode/settings.json | 5 + Cargo.lock | 57 +- Cargo.toml | 2 +- build.rs | 15 + moq | 1 + moq-transport/Cargo.lock | 1315 ++++++++++++++++++ moq-transport/Cargo.toml | 52 + moq-transport/README.md | 10 + moq-transport/src/cache/broadcast.rs | 262 ++++ moq-transport/src/cache/error.rs | 51 + moq-transport/src/cache/fragment.rs | 208 +++ moq-transport/src/cache/mod.rs | 21 + moq-transport/src/cache/segment.rs | 226 +++ moq-transport/src/cache/track.rs | 337 +++++ moq-transport/src/cache/watch.rs | 180 +++ moq-transport/src/coding/decode.rs | 55 + moq-transport/src/coding/encode.rs | 27 + moq-transport/src/coding/mod.rs | 11 + moq-transport/src/coding/params.rs | 85 ++ moq-transport/src/coding/string.rs | 29 + moq-transport/src/coding/varint.rs | 232 +++ moq-transport/src/error.rs | 7 + moq-transport/src/lib.rs | 18 + moq-transport/src/message/announce.rs | 30 + moq-transport/src/message/announce_ok.rs | 23 + moq-transport/src/message/announce_reset.rs | 39 + moq-transport/src/message/go_away.rs | 21 + moq-transport/src/message/mod.rs | 160 +++ moq-transport/src/message/object.rs | 108 ++ moq-transport/src/message/subscribe.rs | 142 ++ moq-transport/src/message/subscribe_error.rs | 36 + moq-transport/src/message/subscribe_fin.rs | 37 + moq-transport/src/message/subscribe_ok.rs | 31 + moq-transport/src/message/subscribe_reset.rs | 50 + moq-transport/src/message/unannounce.rs | 25 + moq-transport/src/message/unsubscribe.rs | 27 + moq-transport/src/session/client.rs | 76 + moq-transport/src/session/control.rs | 45 + moq-transport/src/session/error.rs | 107 ++ moq-transport/src/session/mod.rs | 27 + moq-transport/src/session/publisher.rs | 237 ++++ moq-transport/src/session/server.rs | 116 ++ moq-transport/src/session/subscriber.rs | 211 +++ moq-transport/src/setup/client.rs | 72 + moq-transport/src/setup/extension.rs | 84 ++ moq-transport/src/setup/mod.rs | 17 + moq-transport/src/setup/role.rs | 74 + moq-transport/src/setup/server.rs | 71 + moq-transport/src/setup/version.rs | 155 +++ src/cli.rs | 48 + src/main.rs | 108 +- src/media.rs | 421 ++++++ 53 files changed, 5769 insertions(+), 38 deletions(-) create mode 100644 .gitmodules create mode 100644 .vscode/settings.json create mode 100644 build.rs create mode 160000 moq create mode 100644 moq-transport/Cargo.lock create mode 100644 moq-transport/Cargo.toml create mode 100644 moq-transport/README.md create mode 100644 moq-transport/src/cache/broadcast.rs create mode 100644 moq-transport/src/cache/error.rs create mode 100644 moq-transport/src/cache/fragment.rs create mode 100644 moq-transport/src/cache/mod.rs create mode 100644 moq-transport/src/cache/segment.rs create mode 100644 moq-transport/src/cache/track.rs create mode 100644 moq-transport/src/cache/watch.rs create mode 100644 moq-transport/src/coding/decode.rs create mode 100644 moq-transport/src/coding/encode.rs create mode 100644 moq-transport/src/coding/mod.rs create mode 100644 moq-transport/src/coding/params.rs create mode 100644 moq-transport/src/coding/string.rs create mode 100644 moq-transport/src/coding/varint.rs create mode 100644 moq-transport/src/error.rs create mode 100644 moq-transport/src/lib.rs create mode 100644 moq-transport/src/message/announce.rs create mode 100644 moq-transport/src/message/announce_ok.rs create mode 100644 moq-transport/src/message/announce_reset.rs create mode 100644 moq-transport/src/message/go_away.rs create mode 100644 moq-transport/src/message/mod.rs create mode 100644 moq-transport/src/message/object.rs create mode 100644 moq-transport/src/message/subscribe.rs create mode 100644 moq-transport/src/message/subscribe_error.rs create mode 100644 moq-transport/src/message/subscribe_fin.rs create mode 100644 moq-transport/src/message/subscribe_ok.rs create mode 100644 moq-transport/src/message/subscribe_reset.rs create mode 100644 moq-transport/src/message/unannounce.rs create mode 100644 moq-transport/src/message/unsubscribe.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/error.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 create mode 100644 moq-transport/src/setup/client.rs create mode 100644 moq-transport/src/setup/extension.rs create mode 100644 moq-transport/src/setup/mod.rs create mode 100644 moq-transport/src/setup/role.rs create mode 100644 moq-transport/src/setup/server.rs create mode 100644 moq-transport/src/setup/version.rs create mode 100644 src/cli.rs create mode 100644 src/media.rs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..79d4866 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "moq"] + path = moq + url = https://github.com/kixelated/moq-rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ec17418 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + ".\\Cargo.toml" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 84068ad..edcd40e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,17 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -810,17 +821,16 @@ dependencies = [ [[package]] name = "moq-transport" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2fba4e2add2b2521abfe7dfdd87df1a55805663bfbaf56cb2774ece4183719" dependencies = [ - "anyhow", + "async-trait", "bytes", "indexmap", "log", + "paste", "quinn", "thiserror", "tokio", - "webtransport-quinn 0.5.4", + "webtransport-quinn", ] [[package]] @@ -971,6 +981,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1669,7 +1685,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "webtransport-quinn 0.6.1", + "webtransport-quinn", ] [[package]] @@ -1764,17 +1780,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "webtransport-proto" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d41127a79f4d34112114b626f71d197c3ddf4fc82d56ccddc03a851bd0ea4f" -dependencies = [ - "bytes", - "http", - "thiserror", -] - [[package]] name = "webtransport-proto" version = "0.6.0" @@ -1787,24 +1792,6 @@ dependencies = [ "url", ] -[[package]] -name = "webtransport-quinn" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7cccdcf10a2fb3a18ebd51fb8734e385624cb04fde38b239dbda0f1e40ba21" -dependencies = [ - "async-std", - "bytes", - "futures", - "http", - "quinn", - "quinn-proto", - "thiserror", - "tokio", - "webtransport-generic", - "webtransport-proto 0.5.4", -] - [[package]] name = "webtransport-quinn" version = "0.6.1" @@ -1822,7 +1809,7 @@ dependencies = [ "tokio", "url", "webtransport-generic", - "webtransport-proto 0.6.0", + "webtransport-proto", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ebf65b9..c66b557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -moq-transport = "0.2.0" +moq-transport = { path = "./moq-transport" } # Copied from https://github.com/kixelated/moq-rs/moq-pub quinn = "0.10" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4b663fb --- /dev/null +++ b/build.rs @@ -0,0 +1,15 @@ +include!("src/cli.rs"); + +use clap::CommandFactory; + +fn main() -> Result<(), Box> { + let out_dir = std::path::PathBuf::from( + std::env::var_os("OUT_DIR").ok_or(std::io::Error::new(std::io::ErrorKind::NotFound, "OUT_DIR not found"))?, + ); + let cmd = Config::command(); + let man = clap_mangen::Man::new(cmd); + let mut buffer: Vec = Default::default(); + man.render(&mut buffer)?; + std::fs::write(out_dir.join("moq-pub.1"), buffer)?; + Ok(()) +} \ No newline at end of file diff --git a/moq b/moq new file mode 160000 index 0000000..75e7dc0 --- /dev/null +++ b/moq @@ -0,0 +1 @@ +Subproject commit 75e7dc03bfc0e64fe243742478bd755b399045a2 diff --git a/moq-transport/Cargo.lock b/moq-transport/Cargo.lock new file mode 100644 index 0000000..02d73b0 --- /dev/null +++ b/moq-transport/Cargo.lock @@ -0,0 +1,1315 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1da3ae8dabd9c00f453a329dfe1fb28da3c0a72e2478cdcd93171740c20499" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "moq-transport" +version = "0.2.0" +dependencies = [ + "bytes", + "indexmap", + "log", + "quinn", + "thiserror", + "tokio", + "webtransport-quinn", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "parking" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21252f1c0fc131f1b69182db8f34837e8a69737b8251dff75636a9be0518c324" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85af4ed6ee5a89f26a26086e9089a6643650544c025158449a3626ebf72884b3" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df19e284d93757a9fb91d63672f7741b129246a669db09d1c0063071debc0c0" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.3", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.37.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.4.9", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.16", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" + +[[package]] +name = "web-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webtransport-generic" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df712317d761312996f654739debeb3838eb02c6fd9146d9efdfd08a46674e45" +dependencies = [ + "bytes", + "tokio", +] + +[[package]] +name = "webtransport-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeada5037d6302980ae2e0ab8d840e329c1697c612c6c077172de2b7631a276" +dependencies = [ + "bytes", + "http", + "thiserror", + "url", +] + +[[package]] +name = "webtransport-quinn" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cceb876dbd00a87b3fd8869d1c315e07c28b0eb54d59b592a07a634f5e2b64e1" +dependencies = [ + "async-std", + "bytes", + "futures", + "http", + "quinn", + "quinn-proto", + "thiserror", + "tokio", + "url", + "webtransport-generic", + "webtransport-proto", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +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-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +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" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +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" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +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" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +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" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +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" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +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" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +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" diff --git a/moq-transport/Cargo.toml b/moq-transport/Cargo.toml new file mode 100644 index 0000000..6f5f050 --- /dev/null +++ b/moq-transport/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "moq-transport" +description = "Media over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.2.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] +bytes = "1" +thiserror = "1" +tokio = { version = "1", features = ["macros", "io-util", "sync"] } +log = "0.4" +indexmap = "2" + +quinn = "0.10" +webtransport-quinn = "0.6.1" +#webtransport-quinn = { path = "../../webtransport-rs/webtransport-quinn" } + +async-trait = "0.1" +paste = "1" + +[dev-dependencies] +# QUIC +url = "2" + +# Crypto +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" + +# Async stuff +tokio = { version = "1", features = ["full"] } + +# CLI, logging, error handling +clap = { version = "4", features = ["derive"] } +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +mp4 = "0.13" +anyhow = { version = "1", features = ["backtrace"] } +serde_json = "1" +rfc6381-codec = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" 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/cache/broadcast.rs b/moq-transport/src/cache/broadcast.rs new file mode 100644 index 0000000..feb3824 --- /dev/null +++ b/moq-transport/src/cache/broadcast.rs @@ -0,0 +1,262 @@ +//! 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 [CacheError::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 [CacheError::Closed] when [Publisher] is dropped, or all [Subscriber]s are dropped. +use std::{ + collections::{hash_map, HashMap, VecDeque}, + fmt, + ops::Deref, + sync::Arc, +}; + +use super::{track, CacheError, Watch}; + +/// Create a new broadcast. +pub fn new(id: &str) -> (Publisher, Subscriber) { + let state = Watch::new(State::default()); + let info = Arc::new(Info { id: id.to_string() }); + + let publisher = Publisher::new(state.clone(), info.clone()); + let subscriber = Subscriber::new(state, info); + + (publisher, subscriber) +} + +/// Static information about a broadcast. +#[derive(Debug)] +pub struct Info { + pub id: String, +} + +/// Dynamic information about the broadcast. +#[derive(Debug)] +struct State { + tracks: HashMap, + requested: VecDeque, + closed: Result<(), CacheError>, +} + +impl State { + pub fn get(&self, name: &str) -> Result, CacheError> { + // 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<(), CacheError> { + self.closed.clone()?; + + match self.tracks.entry(track.name.clone()) { + hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate), + hash_map::Entry::Vacant(v) => v.insert(track), + }; + + Ok(()) + } + + pub fn request(&mut self, name: &str) -> Result { + self.closed.clone()?; + + // 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.clone()?; + 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: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + 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, + 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 } + } + + /// 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<(), CacheError> { + self.state.lock_mut().insert(track) + } + + /// Block until the next track requested by a subscriber. + pub async fn next_track(&mut self) -> Result { + loop { + let notify = { + let state = self.state.lock(); + if state.has_next()? { + return Ok(state.into_mut().next()); + } + + state.changed() + }; + + notify.await; + } + } + + /// Close the broadcast with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + 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() + } +} + +/// Subscribe to a broadcast by requesting tracks. +/// +/// This can be cloned to create handles. +#[derive(Clone)] +pub struct Subscriber { + state: Watch, + info: Arc, + _dropped: Arc, +} + +impl Subscriber { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _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 [CacheError::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) + } + + /// Check if the broadcast is closed, either because the publisher was dropped or called [Publisher::close]. + pub fn is_closed(&self) -> Option { + self.state.lock().closed.as_ref().err().cloned() + } + + /// Wait until if the broadcast is closed, either because the publisher was dropped or called [Publisher::close]. + pub async fn closed(&self) -> CacheError { + loop { + let notify = { + let state = self.state.lock(); + if let Some(err) = state.closed.as_ref().err() { + return err.clone(); + } + + 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) + .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(CacheError::Closed).ok(); + } +} diff --git a/moq-transport/src/cache/error.rs b/moq-transport/src/cache/error.rs new file mode 100644 index 0000000..d3f907b --- /dev/null +++ b/moq-transport/src/cache/error.rs @@ -0,0 +1,51 @@ +use thiserror::Error; + +use crate::MoqError; + +#[derive(Clone, Debug, Error)] +pub enum CacheError { + /// A clean termination, represented as error code 0. + /// This error is automatically used when publishers or subscribers are dropped without calling close. + #[error("closed")] + Closed, + + /// An ANNOUNCE_RESET or SUBSCRIBE_RESET was sent by the publisher. + #[error("reset code={0:?}")] + Reset(u32), + + /// An ANNOUNCE_STOP or SUBSCRIBE_STOP was sent by the subscriber. + #[error("stop")] + Stop, + + /// The requested resource was not found. + #[error("not found")] + NotFound, + + /// A resource already exists with that ID. + #[error("duplicate")] + Duplicate, +} + +impl MoqError for CacheError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32 { + match self { + Self::Closed => 0, + Self::Reset(code) => *code, + Self::Stop => 206, + Self::NotFound => 404, + Self::Duplicate => 409, + } + } + + /// A reason that is sent over the wire. + fn reason(&self) -> String { + match self { + Self::Closed => "closed".to_owned(), + Self::Reset(code) => format!("reset code: {}", code), + Self::Stop => "stop".to_owned(), + Self::NotFound => "not found".to_owned(), + Self::Duplicate => "duplicate".to_owned(), + } + } +} diff --git a/moq-transport/src/cache/fragment.rs b/moq-transport/src/cache/fragment.rs new file mode 100644 index 0000000..4e08333 --- /dev/null +++ b/moq-transport/src/cache/fragment.rs @@ -0,0 +1,208 @@ +//! A fragment 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. +//! You can clone the [Subscriber] and each will read a copy of of all future chunks. (fanout) +//! +//! The fragment is closed with [CacheError::Closed] when all publishers or subscribers are dropped. +use core::fmt; +use std::{ops::Deref, sync::Arc}; + +use crate::VarInt; +use bytes::Bytes; + +use super::{CacheError, 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 fragment within the segment. + // NOTE: These may be received out of order or with gaps. + pub sequence: VarInt, + + // The size of the fragment, optionally None if this is the last fragment in a segment. + // TODO enforce this size. + pub size: Option, +} + +struct State { + // The data that has been received thus far. + chunks: Vec, + + // Set when the publisher is dropped. + closed: Result<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self { + chunks: 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. + f.debug_struct("State").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 chunk(&mut self, chunk: Bytes) -> Result<(), CacheError> { + let mut state = self.state.lock_mut(); + state.closed.clone()?; + state.chunks.push(chunk); + Ok(()) + } + + /// Close the segment with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + 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 chunk(&mut self) -> Result, CacheError> { + loop { + let notify = { + let state = self.state.lock(); + if self.index < state.chunks.len() { + let chunk = state.chunks[self.index].clone(); + self.index += 1; + return Ok(Some(chunk)); + } + + match &state.closed { + Err(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + 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(CacheError::Closed).ok(); + } +} diff --git a/moq-transport/src/cache/mod.rs b/moq-transport/src/cache/mod.rs new file mode 100644 index 0000000..96228cf --- /dev/null +++ b/moq-transport/src/cache/mod.rs @@ -0,0 +1,21 @@ +//! Allows a publisher to push updates, automatically caching and fanning it out to any subscribers. +//! +//! The hierarchy is: [broadcast] -> [track] -> [segment] -> [fragment] -> [Bytes](bytes::Bytes) +//! +//! The naming scheme doesn't match the spec because it's more strict, and bikeshedding of course: +//! +//! - [broadcast] is kinda like "track namespace" +//! - [track] is "track" +//! - [segment] is "group" but MUST use a single stream. +//! - [fragment] is "object" but MUST have the same properties as the segment. + +pub mod broadcast; +mod error; +pub mod fragment; +pub mod segment; +pub mod track; + +pub(crate) mod watch; +pub(crate) use watch::*; + +pub use error::*; diff --git a/moq-transport/src/cache/segment.rs b/moq-transport/src/cache/segment.rs new file mode 100644 index 0000000..ecd27de --- /dev/null +++ b/moq-transport/src/cache/segment.rs @@ -0,0 +1,226 @@ +//! A segment is a stream of fragments with a header, split into a [Publisher] and [Subscriber] handle. +//! +//! A [Publisher] writes an ordered stream of fragments. +//! Each fragment can have a sequence number, allowing the subscriber to detect gaps fragments. +//! +//! A [Subscriber] reads an ordered stream of fragments. +//! The subscriber can be cloned, in which case each subscriber receives a copy of each fragment. (fanout) +//! +//! The segment is closed with [CacheError::Closed] when all publishers or subscribers are dropped. +use core::fmt; +use std::{ops::Deref, sync::Arc, time}; + +use crate::VarInt; + +use super::{fragment, CacheError, 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. + // NOTE: These may be received out of order or with gaps. + pub sequence: VarInt, + + // The priority of the segment within the BROADCAST. + pub priority: u32, + + // Cache the segment for at most this long. + pub expires: Option, +} + +struct State { + // The data that has been received thus far. + fragments: Vec, + + // Set when the publisher is dropped. + closed: Result<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self { + fragments: Vec::new(), + closed: Ok(()), + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("State") + .field("fragments", &self.fragments) + .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 } + } + + // Not public because it's a footgun. + pub(crate) fn push_fragment( + &mut self, + sequence: VarInt, + size: Option, + ) -> Result { + let (publisher, subscriber) = fragment::new(fragment::Info { sequence, size }); + + let mut state = self.state.lock_mut(); + state.closed.clone()?; + state.fragments.push(subscriber); + Ok(publisher) + } + + /// Write a fragment + pub fn fragment(&mut self, sequence: VarInt, size: usize) -> Result { + self.push_fragment(sequence, Some(size)) + } + + /// Write the last fragment, which means size can be unknown. + pub fn final_fragment(mut self, sequence: VarInt) -> Result { + self.push_fragment(sequence, None) + } + + /// Close the segment with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + 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 fragment(&mut self) -> Result, CacheError> { + loop { + let notify = { + let state = self.state.lock(); + if self.index < state.fragments.len() { + let fragment = state.fragments[self.index].clone(); + self.index += 1; + return Ok(Some(fragment)); + } + + match &state.closed { + Err(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + 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(CacheError::Closed).ok(); + } +} diff --git a/moq-transport/src/cache/track.rs b/moq-transport/src/cache/track.rs new file mode 100644 index 0000000..6d2d405 --- /dev/null +++ b/moq-transport/src/cache/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 [CacheError::Closed] when all publishers or subscribers are dropped. + +use std::{collections::BinaryHeap, fmt, ops::Deref, sync::Arc, time}; + +use indexmap::IndexMap; + +use super::{segment, CacheError, Watch}; +use crate::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<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } + + pub fn insert(&mut self, segment: segment::Subscriber) -> Result<(), CacheError> { + self.closed.clone()?; + + let entry = match self.lookup.entry(segment.sequence) { + indexmap::map::Entry::Occupied(_entry) => return Err(CacheError::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<(), CacheError> { + 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: CacheError) -> Result<(), CacheError> { + 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 + pub async fn segment(&mut self) -> Result, CacheError> { + 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(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + 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(CacheError::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/cache/watch.rs b/moq-transport/src/cache/watch.rs new file mode 100644 index 0000000..93c8475 --- /dev/null +++ b/moq-transport/src/cache/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/coding/decode.rs b/moq-transport/src/coding/decode.rs new file mode 100644 index 0000000..a6fe94e --- /dev/null +++ b/moq-transport/src/coding/decode.rs @@ -0,0 +1,55 @@ +use super::{BoundsExceeded, VarInt}; +use std::{io, 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 {} +impl AsyncRead for tokio::io::Take<&mut T> where T: AsyncRead {} +impl + Unpin + Send> AsyncRead for io::Cursor {} + +#[async_trait::async_trait] +pub trait Decode: Sized { + async fn decode(r: &mut R) -> Result; +} + +/// A decode error. +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("unexpected end of buffer")] + UnexpectedEnd, + + #[error("invalid string")] + InvalidString(#[from] str::Utf8Error), + + #[error("invalid message: {0:?}")] + InvalidMessage(VarInt), + + #[error("invalid role: {0:?}")] + InvalidRole(VarInt), + + #[error("invalid subscribe location")] + InvalidSubscribeLocation, + + #[error("varint bounds exceeded")] + BoundsExceeded(#[from] BoundsExceeded), + + // TODO move these to ParamError + #[error("duplicate parameter")] + DupliateParameter, + + #[error("missing parameter")] + MissingParameter, + + #[error("invalid parameter")] + InvalidParameter, + + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + // Used to signal that the stream has ended. + #[error("no more messages")] + Final, +} diff --git a/moq-transport/src/coding/encode.rs b/moq-transport/src/coding/encode.rs new file mode 100644 index 0000000..b03cdb9 --- /dev/null +++ b/moq-transport/src/coding/encode.rs @@ -0,0 +1,27 @@ +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 {} +impl AsyncWrite for Vec {} + +#[async_trait::async_trait] +pub trait Encode: Sized { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError>; +} + +/// 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/mod.rs b/moq-transport/src/coding/mod.rs new file mode 100644 index 0000000..ff57b4c --- /dev/null +++ b/moq-transport/src/coding/mod.rs @@ -0,0 +1,11 @@ +mod decode; +mod encode; +mod params; +mod string; +mod varint; + +pub use decode::*; +pub use encode::*; +pub use params::*; +pub use string::*; +pub use varint::*; diff --git a/moq-transport/src/coding/params.rs b/moq-transport/src/coding/params.rs new file mode 100644 index 0000000..9cfd6f3 --- /dev/null +++ b/moq-transport/src/coding/params.rs @@ -0,0 +1,85 @@ +use std::io::Cursor; +use std::{cmp::max, collections::HashMap}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::coding::{AsyncRead, AsyncWrite, Decode, Encode}; + +use crate::{ + coding::{DecodeError, EncodeError}, + VarInt, +}; + +#[derive(Default, Debug, Clone)] +pub struct Params(pub HashMap>); + +#[async_trait::async_trait] +impl Decode for Params { + async fn decode(mut r: &mut R) -> Result { + let mut params = HashMap::new(); + + // I hate this shit so much; let me encode my role and get on with my life. + let count = VarInt::decode(r).await?; + for _ in 0..count.into_inner() { + let kind = VarInt::decode(r).await?; + if params.contains_key(&kind) { + return Err(DecodeError::DupliateParameter); + } + + let size = VarInt::decode(r).await?; + + // Don't allocate the entire requested size to avoid a possible attack + // Instead, we allocate up to 1024 and keep appending as we read further. + let mut pr = r.take(size.into_inner()); + let mut buf = Vec::with_capacity(max(1024, pr.limit() as usize)); + pr.read_to_end(&mut buf).await?; + params.insert(kind, buf); + + r = pr.into_inner(); + } + + Ok(Params(params)) + } +} + +#[async_trait::async_trait] +impl Encode for Params { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::try_from(self.0.len())?.encode(w).await?; + + for (kind, value) in self.0.iter() { + kind.encode(w).await?; + VarInt::try_from(value.len())?.encode(w).await?; + w.write_all(value).await?; + } + + Ok(()) + } +} + +impl Params { + pub fn new() -> Self { + Self::default() + } + + pub async fn set(&mut self, kind: VarInt, p: P) -> Result<(), EncodeError> { + let mut value = Vec::new(); + p.encode(&mut value).await?; + self.0.insert(kind, value); + + Ok(()) + } + + pub fn has(&self, kind: VarInt) -> bool { + self.0.contains_key(&kind) + } + + pub async fn get(&mut self, kind: VarInt) -> Result, DecodeError> { + if let Some(value) = self.0.remove(&kind) { + let mut cursor = Cursor::new(value); + Ok(Some(P::decode(&mut cursor).await?)) + } else { + Ok(None) + } + } +} diff --git a/moq-transport/src/coding/string.rs b/moq-transport/src/coding/string.rs new file mode 100644 index 0000000..2cdff4a --- /dev/null +++ b/moq-transport/src/coding/string.rs @@ -0,0 +1,29 @@ +use std::cmp::min; + +use crate::coding::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::VarInt; + +use super::{Decode, DecodeError, Encode, EncodeError}; + +#[async_trait::async_trait] +impl Encode for String { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + let size = VarInt::try_from(self.len())?; + size.encode(w).await?; + w.write_all(self.as_ref()).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Decode for String { + /// Decode a string with a varint length prefix. + async fn decode(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?; + Ok(str) + } +} diff --git a/moq-transport/src/coding/varint.rs b/moq-transport/src/coding/varint.rs new file mode 100644 index 0000000..8557de8 --- /dev/null +++ b/moq-transport/src/coding/varint.rs @@ -0,0 +1,232 @@ +// Based on quinn-proto +// https://github.com/quinn-rs/quinn/blob/main/quinn-proto/src/varint.rs +// Licensed via Apache 2.0 and MIT + +use std::convert::{TryFrom, TryInto}; +use std::fmt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use super::{Decode, DecodeError, Encode, EncodeError}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)] +#[error("value out of range")] +pub struct BoundsExceeded; + +/// An integer less than 2^62 +/// +/// Values of this type are suitable for encoding as QUIC variable-length integer. +// It would be neat if we could express to Rust that the top two bits are available for use as enum +// discriminants +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +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 { + Self(x as u64) + } + + /// Extract the integer value + pub const fn into_inner(self) -> u64 { + self.0 + } +} + +impl From for u64 { + fn from(x: VarInt) -> Self { + x.0 + } +} + +impl From for usize { + fn from(x: VarInt) -> Self { + x.0 as usize + } +} + +impl From for u128 { + fn from(x: VarInt) -> Self { + x.0 as u128 + } +} + +impl From for VarInt { + fn from(x: u8) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u16) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u32) -> Self { + Self(x.into()) + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: u64) -> Result { + if x <= Self::MAX.into_inner() { + Ok(Self(x)) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: u128) -> Result { + if x <= Self::MAX.into() { + Ok(Self(x as u64)) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: usize) -> Result { + Self::try_from(x as u64) + } +} + +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) + } +} + +impl fmt::Display for VarInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[async_trait::async_trait] +impl Decode for VarInt { + /// Decode a varint from the given reader. + async fn decode(r: &mut R) -> Result { + let b = r.read_u8().await?; + Self::decode_byte(b, r).await + } +} + +impl VarInt { + /// Decode a varint given the first byte, reading the rest as needed. + /// This is silly but useful for determining if the stream has ended. + pub async fn decode_byte(b: u8, r: &mut R) -> Result { + let tag = b >> 6; + + let mut buf = [0u8; 8]; + buf[0] = b & 0b0011_1111; + + let x = match tag { + 0b00 => u64::from(buf[0]), + 0b01 => { + r.read_exact(buf[1..2].as_mut()).await?; + u64::from(u16::from_be_bytes(buf[..2].try_into().unwrap())) + } + 0b10 => { + r.read_exact(buf[1..4].as_mut()).await?; + u64::from(u32::from_be_bytes(buf[..4].try_into().unwrap())) + } + 0b11 => { + r.read_exact(buf[1..8].as_mut()).await?; + u64::from_be_bytes(buf) + } + _ => unreachable!(), + }; + + Ok(Self(x)) + } +} + +#[async_trait::async_trait] +impl Encode for VarInt { + /// Encode a varint to the given writer. + 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?; + } else if x < 2u64.pow(14) { + w.write_u16(0b01 << 14 | x as u16).await?; + } else if x < 2u64.pow(30) { + w.write_u32(0b10 << 30 | x as u32).await?; + } else if x < 2u64.pow(62) { + w.write_u64(0b11 << 62 | x).await?; + } else { + unreachable!("malformed 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..d070251 --- /dev/null +++ b/moq-transport/src/error.rs @@ -0,0 +1,7 @@ +pub trait MoqError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32; + + /// An optional reason sometimes sent over the wire. + fn reason(&self) -> String; +} diff --git a/moq-transport/src/lib.rs b/moq-transport/src/lib.rs new file mode 100644 index 0000000..08f4485 --- /dev/null +++ b/moq-transport/src/lib.rs @@ -0,0 +1,18 @@ +//! 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. +//! +//! This implementation has some required extensions until the draft stablizes. See: [Extensions](crate::setup::Extensions) +mod coding; +mod error; + +pub mod cache; +pub mod message; +pub mod session; +pub mod setup; + +pub use coding::VarInt; +pub use error::MoqError; diff --git a/moq-transport/src/message/announce.rs b/moq-transport/src/message/announce.rs new file mode 100644 index 0000000..281fffa --- /dev/null +++ b/moq-transport/src/message/announce.rs @@ -0,0 +1,30 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the publisher to announce the availability of a group of tracks. +#[derive(Clone, Debug)] +pub struct Announce { + /// The track namespace + pub namespace: String, + + /// Optional parameters + pub params: Params, +} + +impl Announce { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + let params = Params::decode(r).await?; + + Ok(Self { namespace, params }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + self.params.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/announce_ok.rs b/moq-transport/src/message/announce_ok.rs new file mode 100644 index 0000000..300279e --- /dev/null +++ b/moq-transport/src/message/announce_ok.rs @@ -0,0 +1,23 @@ +use crate::{ + coding::{AsyncRead, AsyncWrite, Decode, DecodeError, Encode, EncodeError}, + setup::Extensions, +}; + +/// 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 namespace: String, +} + +impl AnnounceOk { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + Ok(Self { namespace }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(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..24d3f81 --- /dev/null +++ b/moq-transport/src/message/announce_reset.rs @@ -0,0 +1,39 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the subscriber to reject an Announce. +#[derive(Clone, Debug)] +pub struct AnnounceError { + // 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 AnnounceError { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + + Ok(Self { + namespace, + code, + reason, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/go_away.rs b/moq-transport/src/message/go_away.rs new file mode 100644 index 0000000..7999c9a --- /dev/null +++ b/moq-transport/src/message/go_away.rs @@ -0,0 +1,21 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// 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, _ext: &Extensions) -> Result { + let url = String::decode(r).await?; + Ok(Self { url }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.url.encode(w).await + } +} diff --git a/moq-transport/src/message/mod.rs b/moq-transport/src/message/mod.rs new file mode 100644 index 0000000..d32a936 --- /dev/null +++ b/moq-transport/src/message/mod.rs @@ -0,0 +1,160 @@ +//! 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] +//! - [Unannounce] +//! - [SubscribeOk] +//! - [SubscribeError] +//! - [SubscribeReset] +//! - [Object] +//! +//! Messages sent by the subscriber: +//! - [Subscribe] +//! - [Unsubscribe] +//! - [AnnounceOk] +//! - [AnnounceError] +//! +//! 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_ok; +mod announce_reset; +mod go_away; +mod object; +mod subscribe; +mod subscribe_error; +mod subscribe_fin; +mod subscribe_ok; +mod subscribe_reset; +mod unannounce; +mod unsubscribe; + +pub use announce::*; +pub use announce_ok::*; +pub use announce_reset::*; +pub use go_away::*; +pub use object::*; +pub use subscribe::*; +pub use subscribe_error::*; +pub use subscribe_fin::*; +pub use subscribe_ok::*; +pub use subscribe_reset::*; +pub use unannounce::*; +pub use unsubscribe::*; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use std::fmt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +// 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, ext: &Extensions) -> Result { + let t = VarInt::decode(r).await?; + + match t.into_inner() { + $($val => { + let msg = $name::decode(r, ext).await?; + Ok(Self::$name(msg)) + })* + _ => Err(DecodeError::InvalidMessage(t)), + } + } + + pub async fn encode(&self, w: &mut W, ext: &Extensions) -> Result<(), EncodeError> { + match self { + $(Self::$name(ref m) => { + VarInt::from_u32($val).encode(w).await?; + m.encode(w, ext).await + },)* + } + } + + 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 { + fn from(m: $name) -> Self { + Message::$name(m) + } + })* + + impl fmt::Debug for Message { + // Delegate to the message formatter + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Self::$name(ref m) => m.fmt(f),)* + } + } + } + } +} + +// Each message is prefixed with the given VarInt type. +message_types! { + // NOTE: Object and Setup are in other modules. + // Object = 0x0 + // ObjectUnbounded = 0x2 + // SetupClient = 0x40 + // SetupServer = 0x41 + + // SUBSCRIBE family, sent by subscriber + Subscribe = 0x3, + Unsubscribe = 0xa, + + // SUBSCRIBE family, sent by publisher + SubscribeOk = 0x4, + SubscribeError = 0x5, + SubscribeFin = 0xb, + SubscribeReset = 0xc, + + // ANNOUNCE family, sent by publisher + Announce = 0x6, + Unannounce = 0x9, + + // ANNOUNCE family, sent by subscriber + AnnounceOk = 0x7, + AnnounceError = 0x8, + + // Misc + GoAway = 0x10, +} diff --git a/moq-transport/src/message/object.rs b/moq-transport/src/message/object.rs new file mode 100644 index 0000000..90efa23 --- /dev/null +++ b/moq-transport/src/message/object.rs @@ -0,0 +1,108 @@ +use std::{io, time}; + +use tokio::io::AsyncReadExt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup; + +/// 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 group: VarInt, + + // The sequence number within the group. + pub sequence: VarInt, + + // The priority, where **smaller** values are sent first. + pub priority: u32, + + // Cache the object for at most this many seconds. + // Zero means never expire. + pub expires: Option, + + /// An optional size, allowing multiple OBJECTs on the same stream. + pub size: Option, +} + +impl Object { + pub async fn decode(r: &mut R, extensions: &setup::Extensions) -> Result { + // Try reading the first byte, returning a special error if the stream naturally ended. + let typ = match r.read_u8().await { + Ok(b) => VarInt::decode_byte(b, r).await?, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(DecodeError::Final), + Err(e) => return Err(e.into()), + }; + + let size_present = match typ.into_inner() { + 0 => false, + 2 => true, + _ => return Err(DecodeError::InvalidMessage(typ)), + }; + + let track = VarInt::decode(r).await?; + let group = VarInt::decode(r).await?; + let sequence = VarInt::decode(r).await?; + let priority = VarInt::decode(r).await?.try_into()?; + + let expires = match extensions.object_expires { + true => match VarInt::decode(r).await?.into_inner() { + 0 => None, + secs => Some(time::Duration::from_secs(secs)), + }, + false => None, + }; + + // The presence of the size field depends on the type. + let size = match size_present { + true => Some(VarInt::decode(r).await?), + false => None, + }; + + Ok(Self { + track, + group, + sequence, + priority, + expires, + size, + }) + } + + pub async fn encode(&self, w: &mut W, extensions: &setup::Extensions) -> Result<(), EncodeError> { + // The kind changes based on the presence of the size. + let kind = match self.size { + Some(_) => VarInt::from_u32(2), + None => VarInt::ZERO, + }; + + kind.encode(w).await?; + self.track.encode(w).await?; + self.group.encode(w).await?; + self.sequence.encode(w).await?; + VarInt::from_u32(self.priority).encode(w).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(), + }; + + if extensions.object_expires { + VarInt::try_from(expires)?.encode(w).await?; + } + + if let Some(size) = self.size { + size.encode(w).await?; + } + + Ok(()) + } +} diff --git a/moq-transport/src/message/subscribe.rs b/moq-transport/src/message/subscribe.rs new file mode 100644 index 0000000..e64d5a1 --- /dev/null +++ b/moq-transport/src/message/subscribe.rs @@ -0,0 +1,142 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// 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 id: VarInt, + + /// The track namespace. + /// + /// Must be None if `extensions.subscribe_split` is false. + pub namespace: Option, + + /// The track name. + pub name: String, + + /// The start/end group/object. + pub start_group: SubscribeLocation, + pub start_object: SubscribeLocation, + pub end_group: SubscribeLocation, + pub end_object: SubscribeLocation, + + /// Optional parameters + pub params: Params, +} + +impl Subscribe { + pub async fn decode(r: &mut R, ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + + let namespace = match ext.subscribe_split { + true => Some(String::decode(r).await?), + false => None, + }; + + let name = String::decode(r).await?; + + let start_group = SubscribeLocation::decode(r).await?; + let start_object = SubscribeLocation::decode(r).await?; + let end_group = SubscribeLocation::decode(r).await?; + let end_object = SubscribeLocation::decode(r).await?; + + // You can't have a start object without a start group. + if start_group == SubscribeLocation::None && start_object != SubscribeLocation::None { + return Err(DecodeError::InvalidSubscribeLocation); + } + + // You can't have an end object without an end group. + if end_group == SubscribeLocation::None && end_object != SubscribeLocation::None { + return Err(DecodeError::InvalidSubscribeLocation); + } + + // NOTE: There's some more location restrictions in the draft, but they're enforced at a higher level. + + let params = Params::decode(r).await?; + + Ok(Self { + id, + namespace, + name, + start_group, + start_object, + end_group, + end_object, + params, + }) + } + + pub async fn encode(&self, w: &mut W, ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + + if self.namespace.is_some() != ext.subscribe_split { + panic!("namespace must be None if subscribe_split is false"); + } + + if ext.subscribe_split { + self.namespace.as_ref().unwrap().encode(w).await?; + } + + self.name.encode(w).await?; + + self.start_group.encode(w).await?; + self.start_object.encode(w).await?; + self.end_group.encode(w).await?; + self.end_object.encode(w).await?; + + self.params.encode(w).await?; + + Ok(()) + } +} + +/// Signal where the subscription should begin, relative to the current cache. +#[derive(Clone, Debug, PartialEq)] +pub enum SubscribeLocation { + None, + Absolute(VarInt), + Latest(VarInt), + Future(VarInt), +} + +impl SubscribeLocation { + pub async fn decode(r: &mut R) -> Result { + let kind = VarInt::decode(r).await?; + + match kind.into_inner() { + 0 => Ok(Self::None), + 1 => Ok(Self::Absolute(VarInt::decode(r).await?)), + 2 => Ok(Self::Latest(VarInt::decode(r).await?)), + 3 => Ok(Self::Future(VarInt::decode(r).await?)), + _ => Err(DecodeError::InvalidSubscribeLocation), + } + } + + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + match self { + Self::None => { + VarInt::from_u32(0).encode(w).await?; + } + Self::Absolute(val) => { + VarInt::from_u32(1).encode(w).await?; + val.encode(w).await?; + } + Self::Latest(val) => { + VarInt::from_u32(2).encode(w).await?; + val.encode(w).await?; + } + Self::Future(val) => { + VarInt::from_u32(3).encode(w).await?; + val.encode(w).await?; + } + } + + Ok(()) + } +} diff --git a/moq-transport/src/message/subscribe_error.rs b/moq-transport/src/message/subscribe_error.rs new file mode 100644 index 0000000..9ef4c91 --- /dev/null +++ b/moq-transport/src/message/subscribe_error.rs @@ -0,0 +1,36 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to reject a Subscribe. +#[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 subscription. + pub id: VarInt, + + // An error code. + pub code: u32, + + // An optional, human-readable reason. + pub reason: String, +} + +impl SubscribeError { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + + Ok(Self { id, code, reason }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/subscribe_fin.rs b/moq-transport/src/message/subscribe_fin.rs new file mode 100644 index 0000000..b070971 --- /dev/null +++ b/moq-transport/src/message/subscribe_fin.rs @@ -0,0 +1,37 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to cleanly terminate a Subscribe. +#[derive(Clone, Debug)] +pub struct SubscribeFin { + // 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, + + /// The final group/object sent on this subscription. + pub final_group: VarInt, + pub final_object: VarInt, +} + +impl SubscribeFin { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let final_group = VarInt::decode(r).await?; + let final_object = VarInt::decode(r).await?; + + Ok(Self { + id, + final_group, + final_object, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + self.final_group.encode(w).await?; + self.final_object.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/subscribe_ok.rs b/moq-transport/src/message/subscribe_ok.rs new file mode 100644 index 0000000..11864e6 --- /dev/null +++ b/moq-transport/src/message/subscribe_ok.rs @@ -0,0 +1,31 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// 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 id: VarInt, + + /// The subscription will expire in this many milliseconds. + pub expires: VarInt, +} + +impl SubscribeOk { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let expires = VarInt::decode(r).await?; + Ok(Self { id, expires }) + } +} + +impl SubscribeOk { + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + self.expires.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..e488b28 --- /dev/null +++ b/moq-transport/src/message/subscribe_reset.rs @@ -0,0 +1,50 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to terminate 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, + + /// The final group/object sent on this subscription. + pub final_group: VarInt, + pub final_object: VarInt, +} + +impl SubscribeReset { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + let final_group = VarInt::decode(r).await?; + let final_object = VarInt::decode(r).await?; + + Ok(Self { + id, + code, + reason, + final_group, + final_object, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + self.final_group.encode(w).await?; + self.final_object.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/unannounce.rs b/moq-transport/src/message/unannounce.rs new file mode 100644 index 0000000..a2c2e39 --- /dev/null +++ b/moq-transport/src/message/unannounce.rs @@ -0,0 +1,25 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the publisher to terminate an Announce. +#[derive(Clone, Debug)] +pub struct Unannounce { + // Echo back the namespace that was reset + pub namespace: String, +} + +impl Unannounce { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + + Ok(Self { namespace }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/message/unsubscribe.rs b/moq-transport/src/message/unsubscribe.rs new file mode 100644 index 0000000..5361f59 --- /dev/null +++ b/moq-transport/src/message/unsubscribe.rs @@ -0,0 +1,27 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the subscriber to terminate a Subscribe. +#[derive(Clone, Debug)] +pub struct Unsubscribe { + // 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 Unsubscribe { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + Ok(Self { id }) + } +} + +impl Unsubscribe { + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + Ok(()) + } +} diff --git a/moq-transport/src/session/client.rs b/moq-transport/src/session/client.rs new file mode 100644 index 0000000..243fe4e --- /dev/null +++ b/moq-transport/src/session/client.rs @@ -0,0 +1,76 @@ +use super::{Control, Publisher, SessionError, Subscriber}; +use crate::{cache::broadcast, setup}; +use webtransport_quinn::Session; + +/// 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) -> 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) -> 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) -> Result { + let mut control = session.open_bi().await?; + + let versions: setup::Versions = [setup::Version::DRAFT_01, setup::Version::KIXEL_01].into(); + + let client = setup::Client { + role, + versions: versions.clone(), + params: Default::default(), + + // Offer all extensions + extensions: setup::Extensions { + object_expires: true, + subscriber_id: true, + subscribe_split: true, + }, + }; + + log::debug!("sending client SETUP: {:?}", client); + client.encode(&mut control.0).await?; + + let mut server = setup::Server::decode(&mut control.1).await?; + + log::debug!("received server SETUP: {:?}", server); + + match server.version { + setup::Version::DRAFT_01 => { + // We always require this extension + server.extensions.require_subscriber_id()?; + + if server.role.is_publisher() { + // We only require object expires if we're a subscriber, so we don't cache objects indefinitely. + server.extensions.require_object_expires()?; + } + } + setup::Version::KIXEL_01 => { + // KIXEL_01 didn't support extensions; all were enabled. + server.extensions = client.extensions.clone() + } + _ => return Err(SessionError::Version(versions, [server.version].into())), + } + + let control = Control::new(control.0, control.1, server.extensions); + + Ok(control) + } +} diff --git a/moq-transport/src/session/control.rs b/moq-transport/src/session/control.rs new file mode 100644 index 0000000..0686650 --- /dev/null +++ b/moq-transport/src/session/control.rs @@ -0,0 +1,45 @@ +// 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 super::SessionError; +use crate::{message::Message, setup::Extensions}; + +#[derive(Debug, Clone)] +pub(crate) struct Control { + send: Arc>, + recv: Arc>, + pub ext: Extensions, +} + +impl Control { + pub fn new(send: SendStream, recv: RecvStream, ext: Extensions) -> Self { + Self { + send: Arc::new(Mutex::new(send)), + recv: Arc::new(Mutex::new(recv)), + ext, + } + } + + pub async fn send + fmt::Debug>(&self, msg: T) -> Result<(), SessionError> { + let mut stream = self.send.lock().await; + log::info!("sending message: {:?}", msg); + msg.into() + .encode(&mut *stream, &self.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + Ok(()) + } + + // 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; + let msg = Message::decode(&mut *stream, &self.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + Ok(msg) + } +} diff --git a/moq-transport/src/session/error.rs b/moq-transport/src/session/error.rs new file mode 100644 index 0000000..228a4c8 --- /dev/null +++ b/moq-transport/src/session/error.rs @@ -0,0 +1,107 @@ +use crate::{cache, coding, setup, MoqError, VarInt}; + +#[derive(thiserror::Error, Debug)] +pub enum SessionError { + #[error("webtransport error: {0}")] + Session(#[from] webtransport_quinn::SessionError), + + #[error("cache error: {0}")] + Cache(#[from] cache::CacheError), + + #[error("encode error: {0}")] + Encode(#[from] coding::EncodeError), + + #[error("decode error: {0}")] + Decode(#[from] coding::DecodeError), + + #[error("unsupported versions: client={0:?} server={1:?}")] + Version(setup::Versions, setup::Versions), + + #[error("incompatible roles: client={0:?} server={1:?}")] + RoleIncompatible(setup::Role, setup::Role), + + /// An error occured while reading from the QUIC stream. + #[error("failed to read from stream: {0}")] + Read(#[from] webtransport_quinn::ReadError), + + /// An error occured while writing to the QUIC stream. + #[error("failed to write to stream: {0}")] + Write(#[from] webtransport_quinn::WriteError), + + /// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT. + #[error("role violation: msg={0}")] + RoleViolation(VarInt), + + /// Our enforced stream mapping was disrespected. + #[error("stream mapping conflict")] + StreamMapping, + + /// The priority was invalid. + #[error("invalid priority: {0}")] + InvalidPriority(VarInt), + + /// The size was invalid. + #[error("invalid size: {0}")] + InvalidSize(VarInt), + + /// A required extension was not offered. + #[error("required extension not offered: {0:?}")] + RequiredExtension(VarInt), + + /// Some VarInt was too large and we were too lazy to handle it + #[error("varint bounds exceeded")] + BoundsExceeded(#[from] coding::BoundsExceeded), + + /// An unclassified error because I'm lazy. TODO classify these errors + #[error("unknown error: {0}")] + Unknown(String), +} + +impl MoqError for SessionError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32 { + match self { + Self::Cache(err) => err.code(), + Self::RoleIncompatible(..) => 406, + Self::RoleViolation(..) => 405, + Self::StreamMapping => 409, + Self::Unknown(_) => 500, + Self::Write(_) => 501, + Self::Read(_) => 502, + Self::Session(_) => 503, + Self::Version(..) => 406, + Self::Encode(_) => 500, + Self::Decode(_) => 500, + Self::InvalidPriority(_) => 400, + Self::InvalidSize(_) => 400, + Self::RequiredExtension(_) => 426, + Self::BoundsExceeded(_) => 500, + } + } + + /// A reason that is sent over the wire. + fn reason(&self) -> String { + match self { + Self::Cache(err) => err.reason(), + Self::RoleViolation(kind) => format!("role violation for message type {:?}", kind), + Self::RoleIncompatible(client, server) => { + format!( + "role incompatible: client wanted {:?} but server wanted {:?}", + client, server + ) + } + Self::Read(err) => format!("read error: {}", err), + Self::Write(err) => format!("write error: {}", err), + Self::Session(err) => format!("session error: {}", err), + Self::Unknown(err) => format!("unknown error: {}", err), + Self::Version(client, server) => format!("unsupported versions: client={:?} server={:?}", client, server), + Self::Encode(err) => format!("encode error: {}", err), + Self::Decode(err) => format!("decode error: {}", err), + Self::StreamMapping => "streaming mapping conflict".to_owned(), + Self::InvalidPriority(priority) => format!("invalid priority: {}", priority), + Self::InvalidSize(size) => format!("invalid size: {}", size), + Self::RequiredExtension(id) => format!("required extension was missing: {:?}", id), + Self::BoundsExceeded(_) => "varint bounds exceeded".to_string(), + } + } +} diff --git a/moq-transport/src/session/mod.rs b/moq-transport/src/session/mod.rs new file mode 100644 index 0000000..50b36a9 --- /dev/null +++ b/moq-transport/src/session/mod.rs @@ -0,0 +1,27 @@ +//! 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 error; +mod publisher; +mod server; +mod subscriber; + +pub use client::*; +pub(crate) use control::*; +pub use error::*; +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..cf19bd3 --- /dev/null +++ b/moq-transport/src/session/publisher.rs @@ -0,0 +1,237 @@ +use std::{ + collections::{hash_map, HashMap}, + sync::{Arc, Mutex}, +}; + +use tokio::task::AbortHandle; +use webtransport_quinn::Session; + +use crate::{ + cache::{broadcast, segment, track, CacheError}, + message, + message::Message, + MoqError, VarInt, +}; + +use super::{Control, SessionError}; + +/// 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: Control, source: broadcast::Subscriber) -> Self { + Self { + webtransport, + control, + subscribes: Default::default(), + source, + } + } + + // TODO Serve a broadcast without sending an ANNOUNCE. + // fn serve(&mut self, broadcast: broadcast::Subscriber) -> Result<(), SessionError> { + + // 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<(), SessionError> { + let res = self.run_inner().await; + + // Terminate all active subscribes on error. + self.subscribes + .lock() + .unwrap() + .drain() + .for_each(|(_, abort)| abort.abort()); + + res + } + + pub async fn run_inner(&mut self) -> Result<(), SessionError> { + loop { + tokio::select! { + stream = self.webtransport.accept_uni() => { + stream?; + return Err(SessionError::RoleViolation(VarInt::ZERO)); + } + // NOTE: this is not cancel safe, but it's fine since the other branchs are fatal. + msg = self.control.recv() => { + let msg = msg?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg).await { + log::warn!("message error: {:?} {:?}", err, msg); + } + }, + // No more broadcasts are available. + err = self.source.closed() => { + self.webtransport.close(err.code(), err.reason().as_bytes()); + return Ok(()); + }, + } + } + } + + async fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> { + match msg { + Message::AnnounceOk(msg) => self.recv_announce_ok(msg).await, + Message::AnnounceError(msg) => self.recv_announce_error(msg).await, + Message::Subscribe(msg) => self.recv_subscribe(msg).await, + Message::Unsubscribe(msg) => self.recv_unsubscribe(msg).await, + _ => Err(SessionError::RoleViolation(msg.id())), + } + } + + async fn recv_announce_ok(&mut self, _msg: &message::AnnounceOk) -> Result<(), SessionError> { + // We didn't send an announce. + Err(CacheError::NotFound.into()) + } + + async fn recv_announce_error(&mut self, _msg: &message::AnnounceError) -> Result<(), SessionError> { + // We didn't send an announce. + Err(CacheError::NotFound.into()) + } + + async fn recv_subscribe(&mut self, msg: &message::Subscribe) -> Result<(), SessionError> { + // 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(CacheError::Duplicate.into()), // TODO fatal, because we already started the task + hash_map::Entry::Vacant(entry) => entry.insert(abort), + }; + + self.control + .send(message::SubscribeOk { + id: msg.id, + expires: VarInt::ZERO, + }) + .await + } + + async fn reset_subscribe(&mut self, id: VarInt, err: E) -> Result<(), SessionError> { + let msg = message::SubscribeReset { + id, + code: err.code(), + reason: err.reason(), + + // TODO properly populate these + // But first: https://github.com/moq-wg/moq-transport/issues/313 + final_group: VarInt::ZERO, + final_object: VarInt::ZERO, + }; + + self.control.send(msg).await + } + + fn start_subscribe(&mut self, msg: message::Subscribe) -> Result { + // We currently don't use the namespace field in SUBSCRIBE + // Make sure the namespace is empty if it's provided. + if msg.namespace.as_ref().map_or(false, |namespace| !namespace.is_empty()) { + return Err(CacheError::NotFound.into()); + } + + 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(CacheError::Closed.into()); + 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<(), SessionError> { + // TODO add an Ok method to track::Publisher so we can send SUBSCRIBE_OK + + while let Some(mut segment) = track.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<(), SessionError> { + log::trace!("serving group: {:?}", segment); + + let mut stream = self.webtransport.open_uni().await?; + + // Convert the u32 to a i32, since the Quinn set_priority is signed. + let priority = (segment.priority as i64 - i32::MAX as i64) as i32; + stream.set_priority(priority).ok(); + + while let Some(mut fragment) = segment.fragment().await? { + log::trace!("serving fragment: {:?}", fragment); + + let object = message::Object { + track: id, + + // Properties of the segment + group: segment.sequence, + priority: segment.priority, + expires: segment.expires, + + // Properties of the fragment + sequence: fragment.sequence, + size: fragment.size.map(VarInt::try_from).transpose()?, + }; + + object + .encode(&mut stream, &self.control.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + + while let Some(chunk) = fragment.chunk().await? { + //log::trace!("writing chunk: {:?}", chunk); + stream.write_all(&chunk).await?; + } + } + + Ok(()) + } + + async fn recv_unsubscribe(&mut self, msg: &message::Unsubscribe) -> Result<(), SessionError> { + let abort = self + .subscribes + .lock() + .unwrap() + .remove(&msg.id) + .ok_or(CacheError::NotFound)?; + abort.abort(); + + self.reset_subscribe(msg.id, CacheError::Stop).await + } +} diff --git a/moq-transport/src/session/server.rs b/moq-transport/src/session/server.rs new file mode 100644 index 0000000..215fe94 --- /dev/null +++ b/moq-transport/src/session/server.rs @@ -0,0 +1,116 @@ +use super::{Control, Publisher, SessionError, Subscriber}; +use crate::{cache::broadcast, setup}; + +use webtransport_quinn::{RecvStream, SendStream, Session}; + +/// 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) -> Result { + let mut control = session.accept_bi().await?; + + let mut client = setup::Client::decode(&mut control.1).await?; + + log::debug!("received client SETUP: {:?}", client); + + if client.versions.contains(&setup::Version::DRAFT_01) { + // We always require subscriber ID. + client.extensions.require_subscriber_id()?; + + // We require OBJECT_EXPIRES for publishers only. + if client.role.is_publisher() { + client.extensions.require_object_expires()?; + } + + // We don't require SUBSCRIBE_SPLIT since it's easy enough to support, but it's clearly an oversight. + // client.extensions.require(&Extension::SUBSCRIBE_SPLIT)?; + } else if client.versions.contains(&setup::Version::KIXEL_01) { + // Extensions didn't exist in KIXEL_01, so we set them manually. + client.extensions = setup::Extensions { + object_expires: true, + subscriber_id: true, + subscribe_split: true, + }; + } else { + return Err(SessionError::Version( + client.versions, + [setup::Version::DRAFT_01, setup::Version::KIXEL_01].into(), + )); + } + + 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) -> Result { + let setup = self.setup(setup::Role::Publisher)?; + setup.encode(&mut self.control.0).await?; + + let control = Control::new(self.control.0, self.control.1, setup.extensions); + let publisher = Publisher::new(self.session, control, source); + Ok(publisher) + } + + /// Accept the session as a subscriber only. + pub async fn subscriber(mut self, source: broadcast::Publisher) -> Result { + let setup = self.setup(setup::Role::Subscriber)?; + setup.encode(&mut self.control.0).await?; + + let control = Control::new(self.control.0, self.control.1, setup.extensions); + let subscriber = Subscriber::new(self.session, 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 + } + */ + + fn setup(&mut self, role: setup::Role) -> Result { + let server = setup::Server { + role, + version: setup::Version::DRAFT_01, + extensions: self.client.extensions.clone(), + params: Default::default(), + }; + + log::debug!("sending server SETUP: {:?}", server); + + // 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) { + return Err(SessionError::RoleIncompatible(self.client.role, server.role)); + } + + Ok(server) + } + + /// 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..02b5fbd --- /dev/null +++ b/moq-transport/src/session/subscriber.rs @@ -0,0 +1,211 @@ +use webtransport_quinn::{RecvStream, Session}; + +use std::{ + collections::HashMap, + sync::{atomic, Arc, Mutex}, +}; + +use crate::{ + cache::{broadcast, segment, track, CacheError}, + coding::DecodeError, + message, + message::Message, + session::{Control, SessionError}, + VarInt, +}; + +/// 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: Control, source: broadcast::Publisher) -> Self { + Self { + webtransport, + subscribes: Default::default(), + next: Default::default(), + control, + source, + } + } + + pub async fn run(self) -> Result<(), SessionError> { + 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<(), SessionError> { + loop { + let msg = self.control.recv().await?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg) { + log::warn!("message error: {:?} {:?}", err, msg); + } + } + } + + fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> { + match msg { + Message::Announce(_) => Ok(()), // don't care + Message::Unannounce(_) => Ok(()), // also don't care + Message::SubscribeOk(_msg) => Ok(()), // don't care + Message::SubscribeReset(msg) => self.recv_subscribe_error(msg.id, CacheError::Reset(msg.code)), + Message::SubscribeFin(msg) => self.recv_subscribe_error(msg.id, CacheError::Closed), + Message::SubscribeError(msg) => self.recv_subscribe_error(msg.id, CacheError::Reset(msg.code)), + Message::GoAway(_msg) => unimplemented!("GOAWAY"), + _ => Err(SessionError::RoleViolation(msg.id())), + } + } + + fn recv_subscribe_error(&mut self, id: VarInt, err: CacheError) -> Result<(), SessionError> { + let mut subscribes = self.subscribes.lock().unwrap(); + let subscribe = subscribes.remove(&id).ok_or(CacheError::NotFound)?; + subscribe.close(err)?; + + Ok(()) + } + + async fn run_streams(self) -> Result<(), SessionError> { + loop { + // Accept all incoming unidirectional streams. + let stream = self.webtransport.accept_uni().await?; + 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<(), SessionError> { + // Decode the object on the data stream. + let mut object = message::Object::decode(&mut stream, &self.control.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + + log::trace!("first object: {:?}", object); + + // A new scope is needed because the async compiler is dumb + let mut segment = { + let mut subscribes = self.subscribes.lock().unwrap(); + let track = subscribes.get_mut(&object.track).ok_or(CacheError::NotFound)?; + + track.create_segment(segment::Info { + sequence: object.group, + priority: object.priority, + expires: object.expires, + })? + }; + + log::trace!("received segment: {:?}", segment); + + // Create the first fragment + let mut fragment = segment.push_fragment(object.sequence, object.size.map(usize::from))?; + let mut remain = object.size.map(usize::from); + + loop { + if let Some(0) = remain { + // Decode the next object from the stream. + let next = match message::Object::decode(&mut stream, &self.control.ext).await { + Ok(next) => next, + + // No more objects + Err(DecodeError::Final) => break, + + // Unknown error + Err(err) => return Err(err.into()), + }; + + log::trace!("next object: {:?}", object); + + // NOTE: This is a custom restriction; not part of the moq-transport draft. + // We require every OBJECT to contain the same priority since prioritization is done per-stream. + // We also require every OBJECT to contain the same group so we know when the group ends, and can detect gaps. + if next.priority != object.priority && next.group != object.group { + return Err(SessionError::StreamMapping); + } + + object = next; + + // Create a new object. + fragment = segment.push_fragment(object.sequence, object.size.map(usize::from))?; + remain = object.size.map(usize::from); + + log::trace!("next fragment: {:?}", fragment); + } + + match stream.read_chunk(remain.unwrap_or(usize::MAX), true).await? { + // Unbounded object has ended + None if remain.is_none() => break, + + // Bounded object ended early, oops. + None => return Err(DecodeError::UnexpectedEnd.into()), + + // NOTE: This does not make a copy! + // Bytes are immutable and ref counted. + Some(data) => { + remain = remain.map(|r| r - data.bytes.len()); + + log::trace!("next chunk: {:?}", data); + fragment.chunk(data.bytes)?; + } + } + } + + Ok(()) + } + + async fn run_source(mut self) -> Result<(), SessionError> { + loop { + // NOTE: This returns Closed when the source is closed. + let 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: self.control.ext.subscribe_split.then(|| "".to_string()), + name, + + // TODO correctly support these + start_group: message::SubscribeLocation::Latest(VarInt::ZERO), + start_object: message::SubscribeLocation::Absolute(VarInt::ZERO), + end_group: message::SubscribeLocation::None, + end_object: message::SubscribeLocation::None, + + params: Default::default(), + }; + + self.control.send(msg).await?; + } + } +} diff --git a/moq-transport/src/setup/client.rs b/moq-transport/src/setup/client.rs new file mode 100644 index 0000000..a18eb7d --- /dev/null +++ b/moq-transport/src/setup/client.rs @@ -0,0 +1,72 @@ +use super::{Extensions, Role, Versions}; +use crate::{ + coding::{Decode, DecodeError, Encode, EncodeError, Params}, + VarInt, +}; + +use crate::coding::{AsyncRead, AsyncWrite}; + +/// 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 { + /// The list of supported versions in preferred order. + pub versions: Versions, + + /// Indicate if the client is a publisher, a subscriber, or both. + pub role: Role, + + /// A list of known/offered extensions. + pub extensions: Extensions, + + /// Unknown parameters. + pub params: Params, +} + +impl Client { + /// Decode a client setup message. + pub async fn decode(r: &mut R) -> Result { + let typ = VarInt::decode(r).await?; + if typ.into_inner() != 0x40 { + return Err(DecodeError::InvalidMessage(typ)); + } + + let versions = Versions::decode(r).await?; + let mut params = Params::decode(r).await?; + + let role = params + .get::(VarInt::from_u32(0)) + .await? + .ok_or(DecodeError::MissingParameter)?; + + // Make sure the PATH parameter isn't used + // TODO: This assumes WebTransport support only + if params.has(VarInt::from_u32(1)) { + return Err(DecodeError::InvalidParameter); + } + + let extensions = Extensions::load(&mut params).await?; + + Ok(Self { + versions, + role, + extensions, + params, + }) + } + + /// Encode a server setup message. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::from_u32(0x40).encode(w).await?; + self.versions.encode(w).await?; + + let mut params = self.params.clone(); + params.set(VarInt::from_u32(0), self.role).await?; + self.extensions.store(&mut params).await?; + + params.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/setup/extension.rs b/moq-transport/src/setup/extension.rs new file mode 100644 index 0000000..9e8c8cc --- /dev/null +++ b/moq-transport/src/setup/extension.rs @@ -0,0 +1,84 @@ +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; +use crate::session::SessionError; +use crate::VarInt; +use paste::paste; + +/// This is a custom extension scheme to allow/require draft PRs. +/// +/// By convention, the extension number is the PR number + 0xe0000. + +macro_rules! extensions { + {$($name:ident = $val:expr,)*} => { + #[derive(Clone, Default, Debug)] + pub struct Extensions { + $( + pub $name: bool, + )* + } + + impl Extensions { + pub async fn load(params: &mut Params) -> Result { + let mut extensions = Self::default(); + + $( + if let Some(_) = params.get::(VarInt::from_u32($val)).await? { + extensions.$name = true + } + )* + + Ok(extensions) + } + + pub async fn store(&self, params: &mut Params) -> Result<(), EncodeError> { + $( + if self.$name { + params.set(VarInt::from_u32($val), ExtensionExists{}).await?; + } + )* + + Ok(()) + } + + paste! { + $( + pub fn [](&self) -> Result<(), SessionError> { + match self.$name { + true => Ok(()), + false => Err(SessionError::RequiredExtension(VarInt::from_u32($val))), + } + } + )* + } + } + } +} + +struct ExtensionExists; + +#[async_trait::async_trait] +impl Decode for ExtensionExists { + async fn decode(_r: &mut R) -> Result { + Ok(ExtensionExists {}) + } +} + +#[async_trait::async_trait] +impl Encode for ExtensionExists { + async fn encode(&self, _w: &mut W) -> Result<(), EncodeError> { + Ok(()) + } +} + +extensions! { + // required for publishers: OBJECT contains expires VarInt in seconds: https://github.com/moq-wg/moq-transport/issues/249 + // TODO write up a PR + object_expires = 0xe00f9, + + // required: SUBSCRIBE chooses track ID: https://github.com/moq-wg/moq-transport/pull/258 + subscriber_id = 0xe0102, + + // optional: SUBSCRIBE contains namespace/name tuple: https://github.com/moq-wg/moq-transport/pull/277 + subscribe_split = 0xe0115, +} diff --git a/moq-transport/src/setup/mod.rs b/moq-transport/src/setup/mod.rs new file mode 100644 index 0000000..e7662e7 --- /dev/null +++ b/moq-transport/src/setup/mod.rs @@ -0,0 +1,17 @@ +//! 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 extension; +mod role; +mod server; +mod version; + +pub use client::*; +pub use extension::*; +pub use role::*; +pub use server::*; +pub use version::*; diff --git a/moq-transport/src/setup/role.rs b/moq-transport/src/setup/role.rs new file mode 100644 index 0000000..10b30e0 --- /dev/null +++ b/moq-transport/src/setup/role.rs @@ -0,0 +1,74 @@ +use crate::coding::{AsyncRead, AsyncWrite}; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +/// Indicates the endpoint is a publisher, subscriber, or both. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Role { + Publisher, + Subscriber, + Both, +} + +impl Role { + /// Returns true if the role is publisher. + pub fn is_publisher(&self) -> bool { + match self { + Self::Publisher | Self::Both => true, + Self::Subscriber => false, + } + } + + /// 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 { + fn from(r: Role) -> Self { + VarInt::from_u32(match r { + Role::Publisher => 0x1, + Role::Subscriber => 0x2, + Role::Both => 0x3, + }) + } +} + +impl TryFrom for Role { + type Error = DecodeError; + + fn try_from(v: VarInt) -> Result { + match v.into_inner() { + 0x1 => Ok(Self::Publisher), + 0x2 => Ok(Self::Subscriber), + 0x3 => Ok(Self::Both), + _ => Err(DecodeError::InvalidRole(v)), + } + } +} + +#[async_trait::async_trait] +impl Decode for Role { + /// Decode the role. + async fn decode(r: &mut R) -> Result { + let v = VarInt::decode(r).await?; + v.try_into() + } +} + +#[async_trait::async_trait] +impl Encode for Role { + /// Encode the role. + 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 new file mode 100644 index 0000000..7f73119 --- /dev/null +++ b/moq-transport/src/setup/server.rs @@ -0,0 +1,71 @@ +use super::{Extensions, Role, Version}; +use crate::{ + coding::{Decode, DecodeError, Encode, EncodeError, Params}, + VarInt, +}; + +use crate::coding::{AsyncRead, AsyncWrite}; + +/// 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. + pub version: Version, + + /// Indicate if the server is a publisher, a subscriber, or both. + // Proposal: moq-wg/moq-transport#151 + pub role: Role, + + /// Custom extensions. + pub extensions: Extensions, + + /// Unknown parameters. + pub params: Params, +} + +impl Server { + /// Decode the server setup. + pub async fn decode(r: &mut R) -> Result { + let typ = VarInt::decode(r).await?; + if typ.into_inner() != 0x41 { + return Err(DecodeError::InvalidMessage(typ)); + } + + let version = Version::decode(r).await?; + let mut params = Params::decode(r).await?; + + let role = params + .get::(VarInt::from_u32(0)) + .await? + .ok_or(DecodeError::MissingParameter)?; + + // Make sure the PATH parameter isn't used + if params.has(VarInt::from_u32(1)) { + return Err(DecodeError::InvalidParameter); + } + + let extensions = Extensions::load(&mut params).await?; + + Ok(Self { + version, + role, + extensions, + params, + }) + } + + /// Encode the server setup. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::from_u32(0x41).encode(w).await?; + self.version.encode(w).await?; + + let mut params = self.params.clone(); + params.set(VarInt::from_u32(0), self.role).await?; + self.extensions.store(&mut params).await?; + params.encode(w).await?; + + Ok(()) + } +} diff --git a/moq-transport/src/setup/version.rs b/moq-transport/src/setup/version.rs new file mode 100644 index 0000000..8933039 --- /dev/null +++ b/moq-transport/src/setup/version.rs @@ -0,0 +1,155 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +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); + +impl Version { + /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-00.html + pub const DRAFT_00: Version = Version(VarInt::from_u32(0xff000000)); + + /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-01.html + pub const DRAFT_01: Version = Version(VarInt::from_u32(0xff000001)); + + /// 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)); + + /// Fork of draft-ietf-moq-transport-01. + /// + /// Most of the KIXEL_00 changes made it into the draft, or were reverted. + /// This was only used for a short time until extensions were created. + /// + /// - SUBSCRIBE contains a separate track namespace and track name field (accidental revert). [#277](https://github.com/moq-wg/moq-transport/pull/277) + /// - SUBSCRIBE contains the `track_id` instead of SUBSCRIBE_OK. [#145](https://github.com/moq-wg/moq-transport/issues/145) + /// - SUBSCRIBE_* reference `track_id` the instead of the `track_full_name`. [#145](https://github.com/moq-wg/moq-transport/issues/145) + /// - OBJECT `priority` is still a VarInt, but the max value is a u32 (implementation reasons) + /// - OBJECT messages within the same `group` MUST be on the same QUIC stream. + pub const KIXEL_01: Version = Version(VarInt::from_u32(0xbad01)); +} + +impl From for Version { + fn from(v: VarInt) -> Self { + Self(v) + } +} + +impl From for VarInt { + fn from(v: Version) -> Self { + v.0 + } +} + +impl Version { + /// Decode the version number. + pub async fn decode(r: &mut R) -> Result { + let v = VarInt::decode(r).await?; + Ok(Self(v)) + } + + /// 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(Vec); + +#[async_trait::async_trait] +impl Decode for Versions { + /// Decode the version list. + async fn decode(r: &mut R) -> Result { + let count = VarInt::decode(r).await?.into_inner(); + let mut vs = Vec::new(); + + for _ in 0..count { + let v = Version::decode(r).await?; + vs.push(v); + } + + Ok(Self(vs)) + } +} + +#[async_trait::async_trait] +impl Encode for Versions { + /// Encode the version list. + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + let size: VarInt = self.0.len().try_into()?; + size.encode(w).await?; + + for v in &self.0 { + v.encode(w).await?; + } + + Ok(()) + } +} + +impl Deref for Versions { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Versions { + fn from(vs: Vec) -> Self { + Self(vs) + } +} + +impl From<[Version; N]> for Versions { + fn from(vs: [Version; N]) -> Self { + Self(vs.to_vec()) + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..b49fe90 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,48 @@ +use clap::Parser; +use std::{net, path}; +use url::Url; + +#[derive(Parser, Clone, Debug)] +pub struct Config { + /// Listen for UDP packets on the given address. + #[arg(long, default_value = "[::]:0")] + pub bind: net::SocketAddr, + + /// 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, + + /// 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, + + /// Connect to the given URL starting with https:// + #[arg(value_parser = moq_url)] + pub url: Url, + + /// Use the TLS root CA at this path, encoded as PEM. + /// + /// This value can be provided multiple times for multiple roots. + /// If this is empty, system roots will be used instead + #[arg(long)] + pub tls_root: Vec, + + /// Danger: Disable TLS certificate verification. + /// + /// Fine for local development, but should be used in caution in production. + #[arg(long)] + pub tls_disable_verify: bool, +} + +fn moq_url(s: &str) -> Result { + let url = Url::try_from(s).map_err(|e| e.to_string())?; + + // Make sure the scheme is moq + if url.scheme() != "https" { + return Err("url scheme must be https:// for WebTransport".to_string()); + } + + Ok(url) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a9..8c4dc2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,107 @@ -fn main() { - println!("Hello, world!"); +use std::{fs, io, sync::Arc, time}; + +use anyhow::Context; +use clap::Parser; + +mod cli; +use cli::*; + +mod media; +use media::*; + +use moq_transport::cache::broadcast; + +// TODO: clap complete + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + // 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(); + + let config = Config::parse(); + + let (publisher, subscriber) = broadcast::new(""); + let mut media = Media::new(&config, publisher).await?; + + // Create a list of acceptable root certificates. + let mut roots = rustls::RootCertStore::empty(); + + if config.tls_root.is_empty() { + // Add the platform's native root certificates. + for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + roots + .add(&rustls::Certificate(cert.0)) + .context("failed to add root cert")?; + } + } else { + // Add the specified root certificates. + for root in &config.tls_root { + let root = fs::File::open(root).context("failed to open root cert file")?; + let mut root = io::BufReader::new(root); + + let root = rustls_pemfile::certs(&mut root).context("failed to read root cert")?; + anyhow::ensure!(root.len() == 1, "expected a single root cert"); + let root = rustls::Certificate(root[0].to_owned()); + + roots.add(&root).context("failed to add root cert")?; + } + } + + let mut tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + + // Allow disabling TLS verification altogether. + if config.tls_disable_verify { + let noop = NoCertificateVerification {}; + tls_config.dangerous().set_certificate_verifier(Arc::new(noop)); + } + + 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)?; + endpoint.set_default_client_config(quinn_client_config); + + log::info!("connecting to relay: url={}", config.url); + + let session = webtransport_quinn::connect(&endpoint, &config.url) + .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(()) } + +pub struct NoCertificateVerification {} + +impl rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: time::SystemTime, + ) -> Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} \ No newline at end of file diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..5b9cf07 --- /dev/null +++ b/src/media.rs @@ -0,0 +1,421 @@ +use crate::cli::Config; +use anyhow::{self, Context}; +use moq_transport::cache::{broadcast, fragment, segment, track}; +use moq_transport::VarInt; +use mp4::{self, ReadBox}; +use serde_json::json; +use std::cmp::max; +use std::collections::HashMap; +use std::io::Cursor; +use std::time; +use tokio::io::AsyncReadExt; + +pub struct Media { + // 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, + + // Tracks based on their track ID. + tracks: HashMap, +} + +impl Media { + 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"); + + let moov = read_atom(&mut stdin).await?; + anyhow::ensure!(&moov[4..8] == b"moov", "expected moov atom"); + + let mut init = ftyp; + init.extend(&moov); + + // We're going to parse the moov box. + // We have to read the moov box header to correctly advance the cursor for the mp4 crate. + let mut moov_reader = Cursor::new(&moov); + let moov_header = mp4::BoxHeader::read(&mut moov_reader)?; + + // 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 the catalog track with a single segment. + let mut init_track = broadcast.create_track("0.mp4")?; + let init_segment = init_track.create_segment(segment::Info { + sequence: VarInt::ZERO, + priority: 0, + expires: None, + })?; + + // Create a single fragment, optionally setting the size + let mut init_fragment = init_segment.final_fragment(VarInt::ZERO)?; + + init_fragment.chunk(init.into())?; + + let mut tracks = HashMap::new(); + + for trak in &moov.traks { + let id = trak.tkhd.track_id; + let name = format!("{}.m4s", id); + + let timescale = track_timescale(&moov, id); + + // Store the track publisher in a map so we can update it later. + let track = broadcast.create_track(&name)?; + let track = Track::new(track, timescale); + tracks.insert(id, track); + } + + let mut catalog = broadcast.create_track(".catalog")?; + + // Create the catalog track + Self::serve_catalog(&mut catalog, &init_track.name, &moov)?; + + 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 + let mut current = None; + + loop { + let atom = read_atom(&mut stdin).await?; + + let mut reader = Cursor::new(&atom); + let header = mp4::BoxHeader::read(&mut reader)?; + + match header.name { + mp4::BoxType::MoofBox => { + let moof = mp4::MoofBox::read_box(&mut reader, header.size).context("failed to read MP4")?; + + // Process the moof. + let fragment = Fragment::new(moof)?; + + // Get the track for this moof. + let track = self.tracks.get_mut(&fragment.track).context("failed to find track")?; + + // Save the track ID for the next iteration, which must be a mdat. + anyhow::ensure!(current.is_none(), "multiple moof atoms"); + current.replace(fragment.track); + + // Publish the moof header, creating a new segment if it's a keyframe. + track.header(atom, fragment).context("failed to publish moof")?; + } + mp4::BoxType::MdatBox => { + // Get the track ID from the previous moof. + let track = current.take().context("missing moof")?; + let track = self.tracks.get_mut(&track).context("failed to find track")?; + + // Publish the mdat atom. + track.data(atom).context("failed to publish mdat")?; + } + + _ => { + // Skip unknown atoms + } + } + } + } + + fn serve_catalog( + track: &mut track::Publisher, + init_track_name: &str, + moov: &mp4::MoovBox, + ) -> Result<(), anyhow::Error> { + let segment = track.create_segment(segment::Info { + sequence: VarInt::ZERO, + priority: 0, + expires: None, + })?; + + let mut tracks = Vec::new(); + + for trak in &moov.traks { + let mut track = json!({ + "container": "mp4", + "init_track": init_track_name, + "data_track": format!("{}.m4s", trak.tkhd.track_id), + }); + + let stsd = &trak.mdia.minf.stbl.stsd; + if let Some(avc1) = &stsd.avc1 { + // avc1[.PPCCLL] + // + // let profile = 0x64; + // let constraints = 0x00; + // let level = 0x1f; + let profile = avc1.avcc.avc_profile_indication; + let constraints = avc1.avcc.profile_compatibility; // Not 100% certain here, but it's 0x00 on my current test video + let level = avc1.avcc.avc_level_indication; + + let width = avc1.width; + let height = avc1.height; + + let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); + let codec_str = codec.to_string(); + + track["kind"] = json!("video"); + track["codec"] = json!(codec_str); + track["width"] = json!(width); + track["height"] = json!(height); + } else if let Some(_hev1) = &stsd.hev1 { + // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 + anyhow::bail!("HEVC not yet supported") + } else if let Some(mp4a) = &stsd.mp4a { + let desc = &mp4a + .esds + .as_ref() + .context("missing esds box for MP4a")? + .es_desc + .dec_config; + let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); + + track["kind"] = json!("audio"); + track["codec"] = json!(codec_str); + track["channel_count"] = json!(mp4a.channelcount); + track["sample_rate"] = json!(mp4a.samplerate.value()); + track["sample_size"] = json!(mp4a.samplesize); + + let bitrate = max(desc.max_bitrate, desc.avg_bitrate); + if bitrate > 0 { + track["bit_rate"] = json!(bitrate); + } + } else if let Some(vp09) = &stsd.vp09 { + // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 + let vpcc = &vp09.vpcc; + let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); + + track["kind"] = json!("video"); + track["codec"] = json!(codec_str); + track["width"] = json!(vp09.width); // no idea if this needs to be multiplied + track["height"] = json!(vp09.height); // no idea if this needs to be multiplied + + // TODO Test if this actually works; I'm just guessing based on mp4box.js + anyhow::bail!("VP9 not yet supported") + } else { + // TODO add av01 support: https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L251 + anyhow::bail!("unknown codec for track: {}", trak.tkhd.track_id); + } + + tracks.push(track); + } + + let catalog = json!({ + "tracks": tracks + }); + + let catalog_str = serde_json::to_string_pretty(&catalog)?; + log::info!("catalog: {}", catalog_str); + + // Create a single fragment for the segment. + let mut fragment = segment.final_fragment(VarInt::ZERO)?; + + // Add the segment and add the fragment. + fragment.chunk(catalog_str.into())?; + + Ok(()) + } +} + +// Read a full MP4 atom into a vector. +async fn read_atom(reader: &mut R) -> anyhow::Result> { + // Read the 8 bytes for the size + type + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf).await?; + + // Convert the first 4 bytes into the size. + let size = u32::from_be_bytes(buf[0..4].try_into()?) as u64; + + let mut raw = buf.to_vec(); + + let mut limit = match size { + // Runs until the end of the file. + 0 => reader.take(u64::MAX), + + // The next 8 bytes are the extended size to be used instead. + 1 => { + reader.read_exact(&mut buf).await?; + let size_large = u64::from_be_bytes(buf); + anyhow::ensure!(size_large >= 16, "impossible extended box size: {}", size_large); + + reader.take(size_large - 16) + } + + 2..=7 => { + anyhow::bail!("impossible box size: {}", size) + } + + size => reader.take(size - 8), + }; + + // Append to the vector and return it. + let _read_bytes = limit.read_to_end(&mut raw).await?; + + Ok(raw) +} + +struct Track { + // The track we're producing + track: track::Publisher, + + // The current segment + current: Option, + + // The number of units per second. + timescale: u64, + + // The number of segments produced. + sequence: u64, +} + +impl Track { + fn new(track: track::Publisher, timescale: u64) -> Self { + Self { + track, + sequence: 0, + current: None, + timescale, + } + } + + pub fn header(&mut self, raw: Vec, fragment: Fragment) -> anyhow::Result<()> { + if let Some(current) = self.current.as_mut() { + if !fragment.keyframe { + // Use the existing segment + current.chunk(raw.into())?; + return Ok(()); + } + } + + // Otherwise make a new segment + + // Compute the timestamp in milliseconds. + // Overflows after 583 million years, so we're fine. + let timestamp: u32 = fragment + .timestamp(self.timescale) + .as_millis() + .try_into() + .context("timestamp too large")?; + + // Create a new segment. + let segment = self.track.create_segment(segment::Info { + sequence: VarInt::try_from(self.sequence).context("sequence too large")?, + + // Newer segments are higher priority + priority: u32::MAX.checked_sub(timestamp).context("priority too large")?, + + // Delete segments after 10s. + expires: Some(time::Duration::from_secs(10)), + })?; + + // Create a single fragment for the segment that we will keep appending. + let mut fragment = segment.final_fragment(VarInt::ZERO)?; + + self.sequence += 1; + + // Insert the raw atom into the segment. + fragment.chunk(raw.into())?; + + // Save for the next iteration + self.current = Some(fragment); + + Ok(()) + } + + pub fn data(&mut self, raw: Vec) -> anyhow::Result<()> { + let fragment = self.current.as_mut().context("missing current fragment")?; + fragment.chunk(raw.into())?; + + Ok(()) + } +} + +struct Fragment { + // The track for this fragment. + track: u32, + + // The timestamp of the first sample in this fragment, in timescale units. + timestamp: u64, + + // True if this fragment is a keyframe. + keyframe: bool, +} + +impl Fragment { + fn new(moof: mp4::MoofBox) -> anyhow::Result { + // We can't split the mdat atom, so this is impossible to support + anyhow::ensure!(moof.trafs.len() == 1, "multiple tracks per moof atom"); + let track = moof.trafs[0].tfhd.track_id; + + // Parse the moof to get some timing information to sleep. + let timestamp = sample_timestamp(&moof).expect("couldn't find timestamp"); + + // Detect if we should start a new segment. + let keyframe = sample_keyframe(&moof); + + Ok(Self { + track, + timestamp, + keyframe, + }) + } + + // Convert from timescale units to a duration. + fn timestamp(&self, timescale: u64) -> time::Duration { + time::Duration::from_millis(1000 * self.timestamp / timescale) + } +} + +fn sample_timestamp(moof: &mp4::MoofBox) -> Option { + Some(moof.trafs.first()?.tfdt.as_ref()?.base_media_decode_time) +} + +fn sample_keyframe(moof: &mp4::MoofBox) -> bool { + for traf in &moof.trafs { + // TODO trak default flags if this is None + let default_flags = traf.tfhd.default_sample_flags.unwrap_or_default(); + let trun = match &traf.trun { + Some(t) => t, + None => return false, + }; + + for i in 0..trun.sample_count { + let mut flags = match trun.sample_flags.get(i as usize) { + Some(f) => *f, + None => default_flags, + }; + + if i == 0 && trun.first_sample_flags.is_some() { + flags = trun.first_sample_flags.unwrap(); + } + + // https://chromium.googlesource.com/chromium/src/media/+/master/formats/mp4/track_run_iterator.cc#177 + let keyframe = (flags >> 24) & 0x3 == 0x2; // kSampleDependsOnNoOther + let non_sync = (flags >> 16) & 0x1 == 0x1; // kSampleIsNonSyncSample + + if keyframe && !non_sync { + return true; + } + } + } + + false +} + +// Find the timescale for the given track. +fn track_timescale(moov: &mp4::MoovBox, track_id: u32) -> u64 { + let trak = moov + .traks + .iter() + .find(|trak| trak.tkhd.track_id == track_id) + .expect("failed to find trak"); + + trak.mdia.mdhd.timescale as u64 +} \ No newline at end of file