diff --git a/.changeset/datatrack_core_initial_release.md b/.changeset/datatrack_core_initial_release.md new file mode 100644 index 000000000..bb795a122 --- /dev/null +++ b/.changeset/datatrack_core_initial_release.md @@ -0,0 +1,5 @@ +--- +livekit-datatrack: minor +--- + +# Initial release. diff --git a/Cargo.lock b/Cargo.lock index 0ee806d86..66e4735da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ name = "agent_dispatch" version = "0.1.0" dependencies = [ "clap", - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit-api", "livekit-protocol", "log", @@ -120,7 +120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "libc", ] @@ -142,11 +142,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.11.0", "cc", "cesu8", - "jni", - "jni-sys", + "jni 0.21.1", + "jni-sys 0.3.0", "libc", "log", "ndk 0.9.0", @@ -196,7 +196,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -206,9 +221,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -219,6 +234,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -241,9 +265,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "anymap2" @@ -280,7 +304,7 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -299,7 +323,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -365,7 +389,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_derive", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -377,7 +401,7 @@ dependencies = [ "memchr", "serde", "serde_derive", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -405,9 +429,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -445,7 +469,7 @@ dependencies = [ "futures-lite 2.6.1", "parking", "polling 3.11.0", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -513,7 +537,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -683,7 +707,7 @@ dependencies = [ name = "basic_room" version = "0.1.0" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit", "livekit-api", "log", @@ -694,7 +718,7 @@ dependencies = [ name = "basic_text_stream" version = "0.1.0" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "futures-util", "livekit", "log", @@ -720,7 +744,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.114", + "syn 2.0.117", "which", ] @@ -730,7 +754,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -741,7 +765,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -788,7 +812,7 @@ checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -799,9 +823,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -845,7 +869,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -879,15 +903,15 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -900,7 +924,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -917,9 +941,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bzip2" @@ -947,7 +971,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "polling 3.11.0", "rustix 0.38.44", @@ -957,13 +981,13 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "polling 3.11.0", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "tracing", ] @@ -986,8 +1010,8 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", - "rustix 1.1.3", + "calloop 0.14.4", + "rustix 1.1.4", "wayland-backend", "wayland-client", ] @@ -1032,9 +1056,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1059,9 +1083,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -1087,9 +1111,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -1121,9 +1145,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -1131,11 +1155,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim 0.11.1", @@ -1143,21 +1167,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clipboard-win" @@ -1189,7 +1213,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-foundation 0.10.1", "core-graphics-types 0.2.0", @@ -1226,9 +1250,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -1309,9 +1333,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "convert_case" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] @@ -1400,7 +1424,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "libc", ] @@ -1469,7 +1493,7 @@ dependencies = [ "core-foundation-sys 0.8.7", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.21.1", "js-sys", "libc", "mach2", @@ -1554,9 +1578,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -1589,15 +1613,15 @@ dependencies = [ "openssl-probe 0.1.6", "openssl-sys", "schannel", - "socket2 0.6.2", + "socket2 0.6.3", "windows-sys 0.59.0", ] [[package]] name = "curl-sys" -version = "0.4.85+curl-8.18.0" +version = "0.4.86+curl-8.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0efa6142b5ecc05f6d3eaa39e6af4888b9d3939273fb592c92b7088a8cf3fdb" +checksum = "3a1dd6a487cf4532ce0d801634b82aa2deb7c9c3ed930b9dadfce904df000745" dependencies = [ "cc", "libc", @@ -1638,7 +1662,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1668,7 +1692,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1682,7 +1706,7 @@ dependencies = [ "indexmap 2.13.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1700,7 +1724,7 @@ dependencies = [ "indexmap 2.13.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1709,8 +1733,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1727,17 +1761,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -1776,9 +1834,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1794,6 +1852,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -1814,12 +1878,12 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", ] [[package]] @@ -1830,24 +1894,18 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ "libloading 0.8.9", ] -[[package]] -name = "doc-comment" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" - [[package]] name = "document-features" version = "0.2.12" @@ -1884,6 +1942,18 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dummy" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbcf21279103a67372982cb1156a2154a452451dff2b884cf897ccecce389e0" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1989,7 +2059,7 @@ checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" dependencies = [ "accesskit", "ahash", - "bitflags 2.10.0", + "bitflags 2.11.0", "emath", "epaint", "log", @@ -2093,7 +2163,7 @@ dependencies = [ name = "encrypted_text_stream" version = "0.1.0" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "futures-util", "livekit", "livekit-api", @@ -2109,14 +2179,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -2134,11 +2204,11 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "env_filter", "jiff", @@ -2187,7 +2257,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2254,6 +2324,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fake" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b0902eb36fbab51c14eda1c186bda119fcff91e5e4e7fc2dd2077298197ce8" +dependencies = [ + "deunicode", + "dummy", + "either", + "rand 0.9.2", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2286,7 +2368,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2327,9 +2409,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -2345,9 +2427,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2410,7 +2492,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2449,7 +2531,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -2476,9 +2558,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2491,9 +2573,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2501,15 +2583,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2518,9 +2600,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -2552,32 +2634,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2587,15 +2669,14 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -2608,7 +2689,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link", ] @@ -2634,11 +2715,24 @@ dependencies = [ "cfg-if 1.0.4", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -2685,7 +2779,7 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -2710,7 +2804,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2790,7 +2884,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-alloc-types", ] @@ -2800,7 +2894,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2835,7 +2929,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2846,7 +2940,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3123,13 +3217,13 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -3162,14 +3256,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -3178,7 +3271,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3289,6 +3382,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3318,9 +3417,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -3336,8 +3435,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -3411,14 +3510,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3512,15 +3611,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -3531,13 +3630,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3549,19 +3648,68 @@ dependencies = [ "cesu8", "cfg-if 1.0.4", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if 1.0.4", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -3574,9 +3722,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3654,6 +3802,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -3662,9 +3816,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" @@ -3704,13 +3858,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.0", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -3718,9 +3873,9 @@ name = "libwebrtc" version = "0.3.26" dependencies = [ "cxx", - "env_logger 0.11.8", + "env_logger 0.11.9", "glib", - "jni", + "jni 0.21.1", "js-sys", "lazy_static", "livekit-protocol", @@ -3740,9 +3895,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "libc", @@ -3767,17 +3922,16 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "liquid" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9338405fdbc0bce9b01695b2a2ef6b20eca5363f385d47bce48ddf8323cc25" +checksum = "2a494c3f9dad3cb7ed16f1c51812cbe4b29493d6c2e5cd1e2b87477263d9534d" dependencies = [ - "doc-comment", "liquid-core", "liquid-derive", "liquid-lib", @@ -3786,15 +3940,14 @@ dependencies = [ [[package]] name = "liquid-core" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb8fed70857010ed9016ed2ce5a7f34e7cc51d5d7255c9c9dc2e3243e490b42" +checksum = "fc623edee8a618b4543e8e8505584f4847a4e51b805db1af6d9af0a3395d0d57" dependencies = [ "anymap2", - "itertools 0.13.0", + "itertools 0.14.0", "kstring", "liquid-derive", - "num-traits", "pest", "pest_derive", "regex", @@ -3804,24 +3957,23 @@ dependencies = [ [[package]] name = "liquid-derive" -version = "0.26.8" +version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b51f1d220e3fa869e24cfd75915efe3164bd09bb11b3165db3f37f57bf673e3" +checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "liquid-lib" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1794b5605e9f8864a8a4f41aa97976b42512cc81093f8c885d29fb94c6c556" +checksum = "9befeedd61f5995bc128c571db65300aeb50d62e4f0542c88282dbcb5f72372a" dependencies = [ - "itertools 0.13.0", + "itertools 0.14.0", "liquid-core", - "once_cell", "percent-encoding", "regex", "time", @@ -3898,6 +4050,26 @@ dependencies = [ "url", ] +[[package]] +name = "livekit-datatrack" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "fake", + "from_variants", + "futures-core", + "futures-util", + "livekit-protocol", + "livekit-runtime", + "log", + "rand 0.9.2", + "test-case", + "thiserror 2.0.18", + "tokio", + "tokio-stream", +] + [[package]] name = "livekit-ffi" version = "0.12.49" @@ -3906,11 +4078,11 @@ dependencies = [ "console-subscriber", "dashmap", "downcast-rs", - "env_logger 0.11.8", + "env_logger 0.11.9", "from_variants", "futures-util", "imgproc", - "jni", + "jni 0.21.1", "lazy_static", "link-cplusplus", "livekit", @@ -3985,7 +4157,7 @@ dependencies = [ "anyhow", "clap", "cpal", - "env_logger 0.11.8", + "env_logger 0.11.9", "futures-util", "libwebrtc", "livekit", @@ -4004,7 +4176,7 @@ dependencies = [ "eframe", "egui", "egui-wgpu", - "env_logger 0.11.8", + "env_logger 0.11.9", "futures", "image", "libwebrtc", @@ -4012,7 +4184,7 @@ dependencies = [ "livekit-api", "log", "nokhwa", - "objc2 0.6.3", + "objc2 0.6.4", "parking_lot", "tokio", "webrtc-sys", @@ -4115,15 +4287,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -4149,7 +4321,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -4164,7 +4336,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -4212,7 +4384,7 @@ version = "0.1.0" dependencies = [ "android_logger", "futures", - "jni", + "jni 0.21.1", "lazy_static", "livekit", "log", @@ -4223,9 +4395,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -4245,7 +4417,7 @@ checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", "bit-set 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "cfg_aliases", "codespan-reporting 0.12.0", @@ -4271,7 +4443,7 @@ checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" dependencies = [ "arrayvec", "bit-set 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "cfg_aliases", "codespan-reporting 0.12.0", @@ -4300,11 +4472,11 @@ dependencies = [ [[package]] name = "napi" -version = "3.8.2" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909805cbad4d569e69b80e101290fe72e92b9742ba9e333b0c1e83b22fb7447b" +checksum = "e6944d0bf100571cd6e1a98a316cdca262deb6fccf8d93f5ae1502ca3fc88bd3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "ctor", "futures", "napi-build", @@ -4322,29 +4494,29 @@ checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" [[package]] name = "napi-derive" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04ba21bbdf40b33496b4ee6eadfc64d17a6a6cde57cd31549117b0882d1fef86" +checksum = "2c914b5e420182bfb73504e0607592cdb8e2e21437d450883077669fb72a114d" dependencies = [ "convert_case", "ctor", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "napi-derive-backend" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a63791e230572c3218a7acd86ca0a0529fc64294bcbea567cf906d7b04e077" +checksum = "f0864cf6a82e2cfb69067374b64c9253d7e910e5b34db833ed7495dda56ccb18" dependencies = [ "convert_case", "proc-macro2", "quote", "semver", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4358,17 +4530,17 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] @@ -4409,8 +4581,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.11.0", + "jni-sys 0.3.0", "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum", @@ -4423,8 +4595,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.11.0", + "jni-sys 0.3.0", "log", "ndk-sys 0.6.0+11769913", "num_enum", @@ -4444,7 +4616,7 @@ version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -4453,7 +4625,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -4468,7 +4640,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4638,7 +4810,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4684,9 +4856,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -4694,14 +4866,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4732,9 +4904,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -4745,7 +4917,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4761,8 +4933,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", "objc2-core-graphics", "objc2-foundation 0.3.2", ] @@ -4773,7 +4945,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location 0.2.2", @@ -4786,8 +4958,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4808,7 +4980,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4820,7 +4992,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4830,9 +5002,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -4841,9 +5013,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -4866,7 +5038,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4888,7 +5060,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4898,8 +5070,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -4916,7 +5088,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "dispatch", "libc", @@ -4929,10 +5101,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4942,8 +5114,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4965,7 +5137,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4977,7 +5149,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4990,8 +5162,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -5012,7 +5184,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit 0.2.2", @@ -5033,9 +5205,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-cloud-kit 0.3.2", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -5065,7 +5237,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location 0.2.2", @@ -5078,7 +5250,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -5106,7 +5278,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni", + "jni 0.21.1", "ndk 0.8.0", "ndk-context", "num-derive", @@ -5125,9 +5297,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -5137,11 +5309,11 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "foreign-types 0.3.2", "libc", @@ -5158,7 +5330,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5184,9 +5356,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -5197,9 +5369,9 @@ dependencies = [ [[package]] name = "orbclient" -version = "0.3.50" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" dependencies = [ "libc", "libredox", @@ -5207,9 +5379,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.6.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" dependencies = [ "num-traits", ] @@ -5252,7 +5424,7 @@ dependencies = [ "android_system_properties", "log", "nix", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.2", "serde", @@ -5446,7 +5618,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5482,29 +5654,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -5514,9 +5686,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand 2.3.0", @@ -5560,7 +5732,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" name = "play_from_disk" version = "0.1.0" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit", "log", "thiserror 1.0.69", @@ -5569,11 +5741,11 @@ dependencies = [ [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -5606,7 +5778,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -5618,15 +5790,15 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -5668,7 +5840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5691,9 +5863,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -5723,7 +5895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5773,7 +5945,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.114", + "syn 2.0.117", "tempfile", ] @@ -5792,7 +5964,7 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "regex", - "syn 2.0.114", + "syn 2.0.117", "tempfile", ] @@ -5819,7 +5991,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5832,7 +6004,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5864,12 +6036,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "qoi" @@ -5888,9 +6057,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] @@ -5907,8 +6076,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -5917,9 +6086,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -5927,7 +6096,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -5945,16 +6114,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -5965,6 +6134,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -6036,9 +6211,9 @@ dependencies = [ [[package]] name = "range-alloc" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" [[package]] name = "rav1e" @@ -6077,9 +6252,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -6137,23 +6312,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -6163,9 +6338,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -6174,9 +6349,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "renderdoc-sys" @@ -6208,7 +6383,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs 0.8.3", "rustls-pki-types", "serde", @@ -6225,7 +6400,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -6271,7 +6446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" dependencies = [ "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "serde_derive", "unicode-ident", @@ -6281,7 +6456,7 @@ dependencies = [ name = "rpc" version = "0.1.0" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit", "livekit-api", "log", @@ -6377,7 +6552,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6386,14 +6561,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -6411,14 +6586,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -6444,7 +6619,7 @@ dependencies = [ "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework 3.7.0", ] [[package]] @@ -6478,9 +6653,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -6495,9 +6670,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safetensors" @@ -6539,9 +6714,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -6569,7 +6744,7 @@ name = "screensharing" version = "0.1.0" dependencies = [ "clap", - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit", "livekit-api", "log", @@ -6593,7 +6768,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6639,7 +6814,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "core-foundation-sys 0.8.7", "libc", @@ -6648,11 +6823,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys 0.8.7", "libc", @@ -6661,9 +6836,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys 0.8.7", "libc", @@ -6685,7 +6860,7 @@ version = "0.1.0" dependencies = [ "bitfield-struct", "colored", - "env_logger 0.11.8", + "env_logger 0.11.9", "livekit", "log", "rand 0.9.2", @@ -6719,7 +6894,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6819,6 +6994,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -6828,6 +7013,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -6836,9 +7027,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -6878,7 +7069,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -6903,14 +7094,14 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", + "bitflags 2.11.0", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", "memmap2", - "rustix 1.1.3", + "rustix 1.1.4", "thiserror 2.0.18", "wayland-backend", "wayland-client", @@ -6956,12 +7147,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6987,7 +7178,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -7066,9 +7257,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -7098,7 +7289,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7116,9 +7307,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -7133,14 +7324,14 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand 2.3.0", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -7153,13 +7344,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if 1.0.4", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "test-case-core", +] + [[package]] name = "test-log" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" dependencies = [ - "env_logger 0.11.8", + "env_logger 0.11.9", "test-log-macros", "tracing-subscriber", ] @@ -7172,7 +7396,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7210,7 +7434,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7221,7 +7445,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7235,23 +7459,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -7270,9 +7494,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -7315,9 +7539,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -7330,9 +7554,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -7340,7 +7564,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -7358,13 +7582,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7393,7 +7617,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -7406,6 +7630,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -7441,17 +7666,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -7463,32 +7688,41 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ "indexmap 2.13.0", - "toml_datetime", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tonic" @@ -7559,7 +7793,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", @@ -7603,7 +7837,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7639,9 +7873,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -7884,9 +8118,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -7909,6 +8143,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uniffi" version = "0.30.0" @@ -7985,7 +8225,7 @@ dependencies = [ "indexmap 2.13.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8000,7 +8240,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.114", + "syn 2.0.117", "toml", "uniffi_meta", ] @@ -8179,11 +8419,20 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -8194,9 +8443,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -8208,9 +8457,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8218,35 +8467,69 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "wayland-backend" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.3", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -8254,12 +8537,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.12" +version = "0.31.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.3", + "bitflags 2.11.0", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -8270,29 +8553,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.12" +version = "0.31.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" +checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.10" +version = "0.32.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8304,7 +8587,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8313,11 +8596,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" +checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8326,11 +8609,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8339,11 +8622,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8352,9 +8635,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" dependencies = [ "proc-macro2", "quick-xml", @@ -8363,9 +8646,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.8" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" dependencies = [ "dlib", "log", @@ -8375,9 +8658,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -8395,15 +8678,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.22.4", "log", "ndk-context", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "url", "web-sys", @@ -8426,9 +8709,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -8440,7 +8723,7 @@ dependencies = [ "cc", "cxx", "cxx-build", - "env_logger 0.11.8", + "env_logger 0.11.9", "glob", "log", "pkg-config", @@ -8482,7 +8765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if 1.0.4", "cfg_aliases", "document-features", @@ -8511,7 +8794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "cfg-if 1.0.4", "cfg_aliases", @@ -8543,7 +8826,7 @@ dependencies = [ "arrayvec", "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "cfg_aliases", "document-features", @@ -8575,7 +8858,7 @@ dependencies = [ "arrayvec", "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "cfg_aliases", "document-features", @@ -8662,7 +8945,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "bytemuck", "cfg-if 1.0.4", @@ -8711,7 +8994,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.8.0", - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "bytemuck", "cfg-if 1.0.4", @@ -8755,7 +9038,7 @@ version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "js-sys", "log", @@ -8769,7 +9052,7 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "js-sys", "log", @@ -8784,7 +9067,7 @@ dependencies = [ "eframe", "egui", "egui-wgpu", - "env_logger 0.11.8", + "env_logger 0.11.9", "futures", "image", "livekit", @@ -8936,7 +9219,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8947,7 +9230,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8958,7 +9241,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8969,7 +9252,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -9342,14 +9625,14 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -9394,9 +9677,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] @@ -9406,6 +9698,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -9435,7 +9809,7 @@ dependencies = [ "libc", "libloading 0.8.9", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -9452,7 +9826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -9467,7 +9841,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dlib", "log", "once_cell", @@ -9511,7 +9885,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -9528,22 +9902,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -9563,7 +9937,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -9603,7 +9977,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -9628,9 +10002,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" @@ -9661,12 +10035,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -9684,18 +10052,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" dependencies = [ - "zune-core 0.5.1", + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 44b839e68..8c9654142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "livekit-protocol", "livekit-ffi", "livekit-uniffi", + "livekit-datatrack", "livekit-ffi-node-bindings", "livekit-runtime", "livekit-wakeword", @@ -44,6 +45,7 @@ libwebrtc = { version = "0.3.26", path = "libwebrtc" } livekit = { version = "0.7.33", path = "livekit" } livekit-api = { version = "0.4.15", path = "livekit-api" } livekit-ffi = { version = "0.12.49", path = "livekit-ffi" } +livekit-datatrack = { version = "0.1.0", path = "livekit-datatrack" } livekit-protocol = { version = "0.7.1", path = "livekit-protocol" } livekit-runtime = { version = "0.4.0", path = "livekit-runtime" } soxr-sys = { version = "0.1.2", path = "soxr-sys" } @@ -56,7 +58,9 @@ bytes = "1.10" clap = "4.5" console-subscriber = "0.1" env_logger = "0.11" +from_variants = "1.0.2" futures = "0.3" +futures-core = "0.3" futures-util = { version = "0.3", default-features = false } lazy_static = "1.4" log = "0.4" @@ -69,6 +73,7 @@ serde = "1" serde_json = "1.0" thiserror = "1" tokio = { version = "1", default-features = false } +tokio-stream = "0.1" # For examples eframe = { version = "0.33.3", default-features = false } diff --git a/knope.toml b/knope.toml index f4cdef976..16a7ffd46 100644 --- a/knope.toml +++ b/knope.toml @@ -118,3 +118,11 @@ versioned_files = [ { path = "Cargo.toml", dependency = "livekit-wakeword" }, ] changelog = "livekit-wakeword/CHANGELOG.md" + +[packages.livekit-datatrack] +versioned_files = [ + "livekit-datatrack/Cargo.toml", + "Cargo.lock", + { path = "Cargo.toml", dependency = "livekit-datatrack" }, +] +changelog = "livekit-datatrack/CHANGELOG.md" diff --git a/livekit-datatrack/CHANGELOG.md b/livekit-datatrack/CHANGELOG.md new file mode 100644 index 000000000..5ddad421e --- /dev/null +++ b/livekit-datatrack/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog \ No newline at end of file diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml new file mode 100644 index 000000000..c05e48c3c --- /dev/null +++ b/livekit-datatrack/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "livekit-datatrack" +description = "Data track core for LiveKit" +version = "0.1.0" +readme = "README.md" +license.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +livekit-protocol = { workspace = true } +livekit-runtime = { workspace = true } +log = { workspace = true } +thiserror = "2.0.17" # TODO: upgrade workspace version to 2.x.x +tokio = { workspace = true, default-features = false, features = ["sync"] } +futures-util = { workspace = true, default-features = false, features = ["sink"] } +futures-core = { workspace = true } +bytes = { workspace = true } +from_variants = { workspace = true } +tokio-stream = { workspace = true, features = ["sync"] } +anyhow = { workspace = true } # For internal error handling only +rand = { workspace = true } + +[dev-dependencies] +test-case = "3.3" +fake = { version = "4.4", features = ["derive"] } \ No newline at end of file diff --git a/livekit-datatrack/README.md b/livekit-datatrack/README.md new file mode 100644 index 000000000..781b5457d --- /dev/null +++ b/livekit-datatrack/README.md @@ -0,0 +1,6 @@ +# LiveKit Data Track + +**Important**: +This is an internal crate that powers the data tracks feature in LiveKit client SDKs (including [Rust](https://crates.io/crates/livekit) and others) and is not usable directly. + +To use data tracks in your application, please use the public APIs provided by the client SDKs. diff --git a/livekit-datatrack/docs/packet-v0.md b/livekit-datatrack/docs/packet-v0.md new file mode 100644 index 000000000..bda987689 --- /dev/null +++ b/livekit-datatrack/docs/packet-v0.md @@ -0,0 +1,127 @@ +# Data Track Packet (v0) + +This specification outlines the packet format used for data tracks, loosely based on RTP. + +> [!IMPORTANT] +> The structure of data track packets is an internal implementation detail. This specification is relevant only to those working on the internals. + +## Design goals + +This format is designed with the following goals: + +- **Minimal wire size**: compact, binary fields. Any metadata that does not pertain to individual frames should instead be sent via signaling. + +- **High performance encoding/decoding**: in particular, the SFU must be capable of examining potentially tens of thousands of packets per second to make forwarding decisions without fully decoding each packet. + +- **Transport agnostic** + +- **Maximum utilization of the transport's MTU** + +- **Extensibility**: allow for the future addition of new header fields without breaking compatibility with older clients. + +## Structure + +A data track packet consists of the following sections: + +1. Base header +2. Extensions +3. Payload + +### Base header + +```mermaid +packet ++3: "Version (0)" ++1: "S" ++1: "F" ++1: "X" ++10: "Reserved" ++16: "Track Handle" ++16: "Sequence Number" ++16: "Frame Number" ++32: "Timestamp" +``` + +| Name | Bits | Description | +| ---- | ---- | ----------- | +| Version (0) | 3 | Frame header version, initially will be zero. | +| Start Flag (S) | 1 | If set, this is the first packet in a frame. | +| Final Flag (F) | 1 | If set, this is the final packet in a frame. | +| Reserved | 10 | Reserved for future use. | +| Extension Flag (X) | 1 | If set, extensions follow the base header. See format details below. | +| Track Handle | 16 | Unique identifier of the track the frame belongs to, assigned during signaling. Zero is not a valid track identifier. | +| Sequence Number | 16 | Incremented by the publisher for each packet sent, used to detect missing/out-of-order packets. | +| Frame Number | 16 | The frame this packet belongs to. | +| Timestamp | 32 | Equivalent to RTP media timestamp, uses a clock rate of 90K ticks per second. | + +#### Combinations of start and final flag + +- If neither flag is set, this indicates a packet is in the middle of a frame. +- If both flags are set, this indicates a packet is the only one in the frame. + +### Extensions + +If the extension flag in the base header is set, one or more extensions will follow. The format is a variant of [RFC 5285 §4.3](https://datatracker.ietf.org/doc/html/rfc5285#section-4.3) with two notable differences: + +1. There is no fixed-bit pattern following the base header. Instead, it is immediately followed by 16-bit integer indicating total length of all header extensions and padding expressed in number of 32-bit words (i.e., 1 word = 4 bytes) minus one. + +2. Available extensions and their format are defined by this specification rather than out-of-band. The following extensions are currently defined: + +### 1. E2EE (length 13) + +If included, the packet's payload is encrypted using end-to-end encryption. + +| Name | Bits | Description | +| ---- | ---- | ----------- | +| Key Index | 8 | Index into the participant's key ring, used to enable key rotation. | +| IV | 96 | 12-bit AES initialization vector. | + +### 2. User Timestamp (length 8) + +| Name | Bits | Description | +| ---- | ---- | ----------- | +| User Timestamp | 64 | Application-specific frame timestamp, often will be used to associate capture time. Large enough to accommodate a UNIX timestamp | + +## Example + +```mermaid +packet +%% Base header ++3: "Version (0)" ++1: "S" ++1: "F" ++1: "X*" %% Set ++10: "Reserved" ++16: "Track Handle" ++16: "Sequence Number" ++16: "Frame Number" ++32: "Timestamp" + ++16: "Extension Words (7)" + +%% E2EE extension ++8: "ID (2)" ++8: "Length (13)" ++8: "Key Index" ++96: "IV" + +%% User timestamp extension ++8: "ID (1)" ++8: "Length (8)" ++64: "User Timestamp" + ++24: "Padding (0)" + +%% Payload ++ 32: "Payload" +``` + +- 46 bytes total + - Header: 42 bytes + - Payload: 4 bytes +- Note the padding between the two extensions. This is required per [RFC 5285](https://datatracker.ietf.org/doc/html/rfc5285#section-4.3) to ensure the extension block is word aligned. This example shows it placed between the two extensions, but it is allowed before or after any extension. + +## Length calculations + +- Header length (bytes): $L_h=4w+12$, where $w$ is the number of extension words +- Maximum payload length (bytes): $L_{p,max}=L_{mtu}-L_h$ diff --git a/livekit-datatrack/src/e2ee.rs b/livekit-datatrack/src/e2ee.rs new file mode 100644 index 000000000..1f52c40ad --- /dev/null +++ b/livekit-datatrack/src/e2ee.rs @@ -0,0 +1,57 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::Bytes; +use core::fmt::Debug; +use thiserror::Error; + +// TODO: If a core module for end-to-end encryption is created in the future +// (livekit-e2ee), these traits should be moved to there. + +/// Encrypted payload and metadata required for decryption. +pub struct EncryptedPayload { + pub payload: Bytes, + pub iv: [u8; 12], + pub key_index: u8, +} + +/// An error indicating a payload could not be encrypted. +#[derive(Debug, Error)] +#[error("Encryption failed")] +pub struct EncryptionError; + +/// An error indicating a payload could not be decrypted. +#[derive(Debug, Error)] +#[error("Decryption failed")] +pub struct DecryptionError; + +/// Provider for encrypting payloads for E2EE. +pub trait EncryptionProvider: Send + Sync + Debug { + /// Encrypts the given payload being sent by the local participant. + fn encrypt(&self, payload: Bytes) -> Result; +} + +/// Provider for decrypting payloads for E2EE. +pub trait DecryptionProvider: Send + Sync + Debug { + /// Decrypts the given payload received from a remote participant. + /// + /// Sender identity is required in order for the proper key to be used + /// for decryption. + /// + fn decrypt( + &self, + payload: EncryptedPayload, + sender_identity: &str, + ) -> Result; +} diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/error.rs new file mode 100644 index 000000000..521b8a92a --- /dev/null +++ b/livekit-datatrack/src/error.rs @@ -0,0 +1,24 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +/// Internal data track error. +/// +/// Occurrences of this error type are unexpected and likely indicate +/// a bug. If encountered, please report on GitHub and include the full error description. +/// +#[derive(Debug, Error)] +#[error(transparent)] +pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs new file mode 100644 index 000000000..cfa792afa --- /dev/null +++ b/livekit-datatrack/src/frame.rs @@ -0,0 +1,124 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::Bytes; +use core::fmt; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +/// A frame published on a data track, consisting of a payload and optional metadata. +/// +/// # Examples +/// +/// Create a frame from a [`Vec`] payload: +/// +/// ``` +/// # use livekit_datatrack::api::DataTrackFrame; +/// let some_payload = vec![0xFA; 256]; +/// let frame: DataTrackFrame = some_payload.into(); +/// +/// assert_eq!(frame.payload().len(), 256); +/// ``` +/// +#[derive(Clone, Default)] +pub struct DataTrackFrame { + pub(crate) payload: Bytes, + pub(crate) user_timestamp: Option, +} + +impl DataTrackFrame { + /// Returns the frame's payload. + pub fn payload(&self) -> Bytes { + self.payload.clone() // Cheap clone + } + + /// Returns the frame's user timestamp, if one is associated. + pub fn user_timestamp(&self) -> Option { + self.user_timestamp + } + + /// If the frame has a user timestamp, calculate how long has passed + /// relative to the current system time. + /// + /// If a timestamp is present, it is assumed it is a UNIX timestamp in milliseconds + /// (as can be set with [`Self::with_user_timestamp_now`] on the publisher side). + /// If the timestamp is invalid or not present, the result is none. + /// + pub fn duration_since_timestamp(&self) -> Option { + let ts = self.user_timestamp?; + let ts_time = UNIX_EPOCH.checked_add(Duration::from_millis(ts))?; + SystemTime::now() + .duration_since(ts_time) + .inspect_err(|err| log::error!("Failed to calculate duration: {err}")) + .ok() + } +} + +impl DataTrackFrame { + /// Creates a frame from the given payload. + pub fn new(payload: impl Into) -> Self { + Self { payload: payload.into(), ..Default::default() } + } + + /// Associates a user timestamp with the frame. + pub fn with_user_timestamp(mut self, value: u64) -> Self { + self.user_timestamp = Some(value); + self + } + + /// Associates the current Unix timestamp (in milliseconds) with the frame. + pub fn with_user_timestamp_now(mut self) -> Self { + let timestamp = SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .inspect_err(|err| log::error!("Failed to get system time: {err}")) + .ok(); + self.user_timestamp = timestamp; + self + } +} + +impl fmt::Debug for DataTrackFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DataTrackFrame") + .field("payload_len", &self.payload.len()) + .field("user_timestamp", &self.user_timestamp) + .finish() + } +} + +// MARK: - From implementations + +impl From for DataTrackFrame { + fn from(bytes: Bytes) -> Self { + Self { payload: bytes, ..Default::default() } + } +} + +impl From<&'static [u8]> for DataTrackFrame { + fn from(slice: &'static [u8]) -> Self { + Self { payload: slice.into(), ..Default::default() } + } +} + +impl From> for DataTrackFrame { + fn from(vec: Vec) -> Self { + Self { payload: vec.into(), ..Default::default() } + } +} + +impl From> for DataTrackFrame { + fn from(slice: Box<[u8]>) -> Self { + Self { payload: slice.into(), ..Default::default() } + } +} diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs new file mode 100644 index 000000000..573f7e11a --- /dev/null +++ b/livekit-datatrack/src/lib.rs @@ -0,0 +1,59 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![doc = include_str!("../README.md")] + +/// Common types for local and remote tracks. +mod track; + +/// Local track publication. +mod local; + +/// Remote track subscription. +mod remote; + +/// Application-level frame. +mod frame; + +/// Provider for end-to-end encryption/decryption. +mod e2ee; + +/// Data track packet (DTP) format. +mod packet; + +/// Internal utilities. +mod utils; + +/// Internal error. +mod error; + +/// Public APIs re-exported by client SDKs. +pub mod api { + pub use crate::{error::*, frame::*, local::*, remote::*, track::*}; +} + +/// Internal APIs used within client SDKs to power data tracks functionality. +pub mod backend { + pub use crate::e2ee::*; + + /// Local track publication + pub mod local { + pub use crate::local::{events::*, manager::*, proto::*}; + } + + /// Remote track subscription + pub mod remote { + pub use crate::remote::{events::*, manager::*, proto::*}; + } +} diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs new file mode 100644 index 000000000..fda9c1536 --- /dev/null +++ b/livekit-datatrack/src/local/events.rs @@ -0,0 +1,137 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError}, + packet::Handle, +}; +use bytes::Bytes; +use from_variants::FromVariants; +use std::sync::Arc; +use tokio::sync::oneshot; + +/// An external event handled by [`Manager`](super::manager::Manager). +#[derive(Debug, FromVariants)] +pub enum InputEvent { + PublishRequest(PublishRequest), + PublishCancelled(PublishCancelled), + QueryPublished(QueryPublished), + UnpublishRequest(UnpublishRequest), + SfuPublishResponse(SfuPublishResponse), + SfuUnpublishResponse(SfuUnpublishResponse), + /// Republish all tracks. + /// + /// This must be sent after a full reconnect in order for existing publications + /// to be recognized by the SFU. Each republished track will be assigned a new SID. + /// + RepublishTracks, + /// Shutdown the manager and all associated tracks. + Shutdown, +} + +/// An event produced by [`Manager`](super::manager::Manager) requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + SfuPublishRequest(SfuPublishRequest), + SfuUnpublishRequest(SfuUnpublishRequest), + /// Serialized packets are ready to be sent over the transport. + PacketsAvailable(Vec), +} + +// MARK: - Input events + +/// Client requested to publish a track. +/// +/// Send using [`ManagerInput::publish_track`] and await the result. +/// +/// [`ManagerInput::publish_track`]: super::manager::ManagerInput::publish_track +/// +#[derive(Debug)] +pub struct PublishRequest { + /// Publish options. + pub(super) options: DataTrackOptions, + /// Async completion channel. + pub(super) result_tx: oneshot::Sender>, +} + +/// Client request to publish a track has been cancelled (internal). +#[derive(Debug)] +pub struct PublishCancelled { + /// Publisher handle of the pending publication. + pub(super) handle: Handle, +} + +/// Client request to unpublish a track (internal). +#[derive(Debug)] +pub struct UnpublishRequest { + /// Publisher handle of the track to unpublish. + pub(super) handle: Handle, +} + +/// Get information about all currently published tracks. +/// +/// Send using [`ManagerInput::query_tracks`] and await the result. This is used +/// to support sync state. +/// +/// [`ManagerInput::query_tracks`]: super::manager::ManagerInput::query_tracks +/// +#[derive(Debug)] +pub struct QueryPublished { + pub(super) result_tx: oneshot::Sender>>, +} + +/// SFU responded to a request to publish a data track. +/// +/// Protocol equivalent: [`livekit_protocol::PublishDataTrackResponse`]. +/// +#[derive(Debug)] +pub struct SfuPublishResponse { + /// Publisher handle of the track. + pub handle: Handle, + /// Outcome of the publish request. + pub result: Result, +} + +/// SFU notification that a track has been unpublished. +/// +/// Protocol equivalent: [`livekit_protocol::UnpublishDataTrackResponse`]. +/// +#[derive(Debug)] +pub struct SfuUnpublishResponse { + /// Publisher handle of the track that was unpublished. + pub handle: Handle, +} + +// MARK: - Output events + +/// Request sent to the SFU to publish a track. +/// +/// Protocol equivalent: [`livekit_protocol::PublishDataTrackRequest`]. +/// +#[derive(Debug)] +pub struct SfuPublishRequest { + pub handle: Handle, + pub name: String, + pub uses_e2ee: bool, +} + +/// Request sent to the SFU to unpublish a track. +/// +/// Protocol equivalent: [`livekit_protocol::UnpublishDataTrackRequest`]. +/// +#[derive(Debug)] +pub struct SfuUnpublishRequest { + /// Publisher handle of the track to unpublish. + pub handle: Handle, +} diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs new file mode 100644 index 000000000..15d76c318 --- /dev/null +++ b/livekit-datatrack/src/local/manager.rs @@ -0,0 +1,788 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + events::*, + pipeline::{Pipeline, PipelineOptions}, + LocalTrackInner, +}; +use crate::{ + api::{DataTrackFrame, DataTrackInfo, DataTrackOptions, InternalError, PublishError}, + e2ee::EncryptionProvider, + local::LocalDataTrack, + packet::{self, Handle}, +}; +use anyhow::{anyhow, Context}; +use futures_core::Stream; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio_stream::wrappers::ReceiverStream; + +/// Options for creating a [`Manager`]. +#[derive(Debug)] +pub struct ManagerOptions { + /// Provider to use for encrypting outgoing frame payloads. + /// + /// If none, end-to-end encryption will be disabled for all published tracks. + /// + pub encryption_provider: Option>, +} + +/// System for managing data track publications. +pub struct Manager { + encryption_provider: Option>, + event_in_tx: mpsc::Sender, + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, + handle_allocator: packet::HandleAllocator, + descriptors: HashMap, +} + +impl Manager { + /// Creates a new manager. + /// + /// Returns a tuple containing the following: + /// + /// - The manager itself to be spawned by the caller (see [`Manager::run`]). + /// - Channel for sending [`InputEvent`]s to be processed by the manager. + /// - Stream for receiving [`OutputEvent`]s produced by the manager. + /// + pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT); + let (event_out_tx, event_out_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT); + + let event_in = ManagerInput::new(event_in_tx.clone()); + let manager = Manager { + encryption_provider: options.encryption_provider, + event_in_tx, + event_in_rx, + event_out_tx, + handle_allocator: packet::HandleAllocator::default(), + descriptors: HashMap::new(), + }; + + let event_out = ReceiverStream::new(event_out_rx); + (manager, event_in, event_out) + } + + /// Run the manager task, consuming self. + /// + /// The manager will continue running until receiving [`InputEvent::Shutdown`]. + /// + pub async fn run(mut self) { + log::debug!("Task started"); + while let Some(event) = self.event_in_rx.recv().await { + log::debug!("Input event: {:?}", event); + match event { + InputEvent::PublishRequest(event) => self.on_publish_request(event).await, + InputEvent::PublishCancelled(event) => self.on_publish_cancelled(event).await, + InputEvent::QueryPublished(event) => self.on_query_published(event).await, + InputEvent::UnpublishRequest(event) => self.on_unpublish_request(event).await, + InputEvent::SfuPublishResponse(event) => self.on_sfu_publish_response(event).await, + InputEvent::SfuUnpublishResponse(event) => { + self.on_sfu_unpublish_response(event).await + } + InputEvent::RepublishTracks => self.on_republish_tracks().await, + InputEvent::Shutdown => break, + } + } + self.shutdown().await; + log::debug!("Task ended"); + } + + async fn on_publish_request(&mut self, event: PublishRequest) { + let Some(handle) = self.handle_allocator.get() else { + _ = event.result_tx.send(Err(PublishError::LimitReached)); + return; + }; + + if self.descriptors.contains_key(&handle) { + _ = event.result_tx.send(Err(PublishError::Internal( + anyhow!("Descriptor for handle already exists").into(), + ))); + return; + } + + let (result_tx, result_rx) = oneshot::channel(); + self.descriptors.insert(handle, Descriptor::Pending(result_tx)); + + livekit_runtime::spawn(Self::forward_publish_result( + handle, + result_rx, + event.result_tx, + self.event_in_tx.downgrade(), + )); + + let event = SfuPublishRequest { + handle, + name: event.options.name, + uses_e2ee: self.encryption_provider.is_some(), + }; + _ = self.event_out_tx.send(event.into()).await; + } + + /// Task that awaits a pending publish result. + /// + /// Forwards the result to the user, or notifies the manager if the receiver + /// is dropped (e.g., due to timeout) so it can remove the pending publication. + /// + async fn forward_publish_result( + handle: Handle, + result_rx: oneshot::Receiver>, + mut forward_result_tx: oneshot::Sender>, + event_in_tx: mpsc::WeakSender, + ) { + tokio::select! { + biased; + Ok(result) = result_rx => { + _ = forward_result_tx.send(result); + } + _ = forward_result_tx.closed() => { + let Some(tx) = event_in_tx.upgrade() else { return }; + let event = PublishCancelled { handle }; + _ = tx.try_send(event.into()); + } + } + } + + async fn on_publish_cancelled(&mut self, event: PublishCancelled) { + if self.descriptors.remove(&event.handle).is_none() { + log::warn!("No descriptor for {}", event.handle); + } + } + + async fn on_query_published(&self, event: QueryPublished) { + let published_info: Vec<_> = self + .descriptors + .iter() + .filter_map(|descriptor| { + let (_, Descriptor::Active { info, .. }) = descriptor else { + return None; + }; + info.clone().into() + }) + .collect(); + _ = event.result_tx.send(published_info); + } + + async fn on_unpublish_request(&mut self, event: UnpublishRequest) { + self.remove_descriptor(event.handle); + + let event = SfuUnpublishRequest { handle: event.handle }; + _ = self.event_out_tx.send(event.into()).await; + } + + async fn on_sfu_publish_response(&mut self, event: SfuPublishResponse) { + let Some(descriptor) = self.descriptors.remove(&event.handle) else { + // This can occur if a publish request is cancelled before the SFU responds, + // send an unpublish request to ensure consistent SFU state. + _ = self.event_out_tx.send(SfuUnpublishRequest { handle: event.handle }.into()).await; + return; + }; + match descriptor { + Descriptor::Pending(result_tx) => { + // SFU accepted initial publication request + if result_tx.is_closed() { + return; + } + let result = event.result.map(|track_info| self.create_local_track(track_info)); + _ = result_tx.send(result); + return; + } + Descriptor::Active { ref state_tx, ref info, .. } => { + if *state_tx.borrow() != PublishState::Republishing { + log::warn!("Track {} already active", event.handle); + return; + } + let Ok(updated_info) = event.result else { + log::warn!("Republish failed for track {}", event.handle); + return; + }; + + log::debug!("Track {} republished", event.handle); + { + let mut sid = info.sid.write().unwrap(); + *sid = updated_info.sid(); + } + _ = state_tx.send(PublishState::Published); + self.descriptors.insert(event.handle, descriptor); + } + } + } + + fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { + let info = Arc::new(info); + let encryption_provider = + if info.uses_e2ee() { self.encryption_provider.as_ref().map(Arc::clone) } else { None }; + + let pipeline_opts = PipelineOptions { info: info.clone(), encryption_provider }; + let pipeline = Pipeline::new(pipeline_opts); + + let (frame_tx, frame_rx) = mpsc::channel(Self::FRAME_BUFFER_COUNT); + let (state_tx, state_rx) = watch::channel(PublishState::Published); + + let track_task = TrackTask { + info: info.clone(), + pipeline, + state_rx, + frame_rx, + event_in_tx: self.event_in_tx.clone(), + event_out_tx: self.event_out_tx.clone(), + }; + let task_handle = livekit_runtime::spawn(track_task.run()); + + self.descriptors.insert( + info.pub_handle, + Descriptor::Active { info: info.clone(), state_tx: state_tx.clone(), task_handle }, + ); + + let inner = LocalTrackInner { frame_tx, state_tx }; + LocalDataTrack::new(info, inner) + } + + async fn on_sfu_unpublish_response(&mut self, event: SfuUnpublishResponse) { + self.remove_descriptor(event.handle); + } + + fn remove_descriptor(&mut self, handle: Handle) { + let Some(descriptor) = self.descriptors.remove(&handle) else { + return; + }; + let Descriptor::Active { state_tx, .. } = descriptor else { + return; + }; + if *state_tx.borrow() != PublishState::Unpublished { + _ = state_tx.send(PublishState::Unpublished); + } + } + + async fn on_republish_tracks(&mut self) { + let descriptors = std::mem::take(&mut self.descriptors); + for (handle, descriptor) in descriptors { + match descriptor { + Descriptor::Pending(result_tx) => { + // TODO: support republish for pending publications + _ = result_tx.send(Err(PublishError::Disconnected)); + } + Descriptor::Active { ref info, ref state_tx, .. } => { + let event = SfuPublishRequest { + handle: info.pub_handle, + name: info.name.clone(), + uses_e2ee: info.uses_e2ee, + }; + _ = state_tx.send(PublishState::Republishing); + _ = self.event_out_tx.send(event.into()).await; + self.descriptors.insert(handle, descriptor); + } + } + } + } + + /// Performs cleanup before the task ends. + async fn shutdown(self) { + for (_, descriptor) in self.descriptors { + match descriptor { + Descriptor::Pending(result_tx) => { + _ = result_tx.send(Err(PublishError::Disconnected)) + } + Descriptor::Active { state_tx, task_handle, .. } => { + _ = state_tx.send(PublishState::Unpublished); + task_handle.await; + } + } + } + } + + /// Maximum number of outgoing frames to buffer per track. + const FRAME_BUFFER_COUNT: usize = 16; + + /// Maximum number of input and output events to buffer. + const EVENT_BUFFER_COUNT: usize = 16; +} + +/// Task for an individual published data track. +struct TrackTask { + info: Arc, + pipeline: Pipeline, + state_rx: watch::Receiver, + frame_rx: mpsc::Receiver, + event_in_tx: mpsc::Sender, + event_out_tx: mpsc::Sender, +} + +impl TrackTask { + async fn run(mut self) { + let sid = self.info.sid(); + log::debug!("Track task started: sid={}", sid); + + let mut state = *self.state_rx.borrow(); + while state != PublishState::Unpublished { + tokio::select! { + _ = self.state_rx.changed() => { + state = *self.state_rx.borrow(); + } + Some(frame) = self.frame_rx.recv() => { + if state == PublishState::Republishing { + // Drop frames while republishing. + continue; + } + self.process_and_send(frame); + } + } + } + + let event = UnpublishRequest { handle: self.info.pub_handle }; + _ = self.event_in_tx.send(event.into()).await; + + log::debug!("Track task ended: sid={}", sid); + } + + fn process_and_send(&mut self, frame: DataTrackFrame) { + let Ok(packets) = self + .pipeline + .process_frame(frame) + .inspect_err(|err| log::debug!("Process failed: {}", err)) + else { + return; + }; + let packets: Vec<_> = packets.into_iter().map(|packet| packet.serialize()).collect(); + _ = self + .event_out_tx + .try_send(packets.into()) + .inspect_err(|err| log::debug!("Cannot send packets to transport: {}", err)); + } +} + +#[derive(Debug)] +enum Descriptor { + /// Publication is awaiting SFU response. + /// + /// The associated channel is used to send a result to the user, + /// either the local track or a publish error. + /// + Pending(oneshot::Sender>), + /// Publication is active. + /// + /// The associated channel is used to end the track task. + /// + Active { + info: Arc, + state_tx: watch::Sender, + task_handle: livekit_runtime::JoinHandle<()>, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PublishState { + /// Track is published. + Published, + /// Track is being republished. + Republishing, + /// Track is no longer published. + Unpublished, +} + +/// Channel for sending [`InputEvent`]s to [`Manager`]. +#[derive(Debug, Clone)] +pub struct ManagerInput { + event_in_tx: mpsc::Sender, + _drop_guard: Arc, +} + +/// Guard that sends shutdown event when the last reference is dropped. +#[derive(Debug)] +struct DropGuard { + event_in_tx: mpsc::Sender, +} + +impl Drop for DropGuard { + fn drop(&mut self) { + _ = self.event_in_tx.try_send(InputEvent::Shutdown); + } +} + +impl ManagerInput { + fn new(event_in_tx: mpsc::Sender) -> Self { + Self { event_in_tx: event_in_tx.clone(), _drop_guard: DropGuard { event_in_tx }.into() } + } + + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) + } + + /// Publishes a data track with given options. + pub async fn publish_track( + &self, + options: DataTrackOptions, + ) -> Result { + let (result_tx, result_rx) = oneshot::channel(); + + let event = PublishRequest { options, result_tx }; + self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; + + let track = tokio::time::timeout(Self::PUBLISH_TIMEOUT, result_rx) + .await + .map_err(|_| PublishError::Timeout)? + .map_err(|_| PublishError::Disconnected)??; + + Ok(track) + } + + /// Get information about all currently published tracks. + /// + /// This does not include publications that are still pending. + /// + pub async fn query_tracks(&self) -> Vec> { + let (result_tx, result_rx) = oneshot::channel(); + + let event = QueryPublished { result_tx }; + if self.event_in_tx.send(event.into()).await.is_err() { + return vec![]; + } + + result_rx.await.unwrap_or_default() + } + + /// How long to wait for before timeout. + const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + api::DataTrackSid, + e2ee::{EncryptedPayload, EncryptionError, EncryptionProvider}, + packet::Packet, + utils::testing::expect_event, + }; + use bytes::Bytes; + use fake::{Fake, Faker}; + use futures_util::StreamExt; + use livekit_runtime::{sleep, timeout}; + use std::sync::RwLock; + + #[derive(Debug)] + struct PrefixingEncryptor; + + impl EncryptionProvider for PrefixingEncryptor { + fn encrypt(&self, payload: Bytes) -> Result { + let mut output = Vec::with_capacity(4 + payload.len()); + output.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); + output.extend_from_slice(&payload); + Ok(EncryptedPayload { payload: output.into(), iv: [0; 12], key_index: 0 }) + } + } + + #[tokio::test] + async fn test_task_shutdown() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, _) = Manager::new(options); + + let join_handle = livekit_runtime::spawn(manager.run()); + _ = input.send(InputEvent::Shutdown); + + timeout(Duration::from_secs(1), join_handle).await.unwrap(); + } + + #[tokio::test] + async fn test_publish() { + let payload_size = 256; + let packet_count = 10; + + let track_name: String = Faker.fake(); + let track_sid: DataTrackSid = Faker.fake(); + let pub_handle: Handle = Faker.fake(); + + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_name_clone = track_name.clone(); + let handle_events = async { + let mut packets_sent = 0; + while let Some(event) = output.next().await { + match event { + OutputEvent::SfuPublishRequest(event) => { + assert!(!event.uses_e2ee); + assert_eq!(event.name, track_name_clone); + + // SFU accepts publication + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle, + name: event.name, + uses_e2ee: event.uses_e2ee, + }; + let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; + _ = input.send(event.into()); + } + OutputEvent::PacketsAvailable(packets) => { + let packet = packets.into_iter().nth(0).unwrap(); + let payload = Packet::deserialize(packet).unwrap().payload; + assert_eq!(payload.len(), payload_size); + packets_sent += 1; + } + OutputEvent::SfuUnpublishRequest(event) => { + assert_eq!(event.handle, pub_handle); + assert_eq!(packets_sent, packet_count); + break; + } + } + } + }; + let publish_track = async { + let track_options = DataTrackOptions::new(track_name.clone()); + let track = input.publish_track(track_options).await.unwrap(); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), track_name); + assert_eq!(track.info().sid(), track_sid); + + for _ in 0..packet_count { + track.try_push(vec![0xFA; payload_size].into()).unwrap(); + sleep(Duration::from_millis(10)).await; + } + // Only reference to track dropped here (unpublish) + }; + timeout(Duration::from_secs(1), async { tokio::join!(publish_track, handle_events) }) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_publish_sfu_error() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new("test"), result_tx }; + input.send(event.into()).unwrap(); + + // SFU rejects publication + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + let event = + SfuPublishResponse { handle: event.handle, result: Err(PublishError::LimitReached) }; + input.send(event.into()).unwrap(); + + assert!(result_rx.await.unwrap().is_err()); + } + + #[tokio::test] + async fn test_publish_cancelled() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new("test"), result_tx }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + let handle = event.handle; + + // Caller drops receiver before SFU responds + drop(result_rx); + sleep(Duration::from_millis(50)).await; + + // Late SFU response arrives after cancellation + let track_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid).into(), + pub_handle: handle, + name: "test".into(), + uses_e2ee: false, + }; + let event = SfuPublishResponse { handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + + // Manager sends unpublish for the orphaned handle + let event = expect_event!(output, OutputEvent::SfuUnpublishRequest); + assert_eq!(event.handle, handle); + } + + #[tokio::test] + async fn test_publish_with_e2ee() { + let options = ManagerOptions { encryption_provider: Some(Arc::new(PrefixingEncryptor)) }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new("secure"), result_tx }; + input.send(event.into()).unwrap(); + + // SFU publish request should indicate e2ee + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + assert!(event.uses_e2ee); + + // SFU accepts publication with e2ee + let track_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid).into(), + pub_handle: event.handle, + name: "secure".into(), + uses_e2ee: true, + }; + let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + + let track = result_rx.await.unwrap().unwrap(); + assert!(track.info().uses_e2ee()); + + // Push a frame and verify encryption was applied + track.try_push(vec![1, 2, 3, 4, 5].into()).unwrap(); + + let packets = expect_event!(output, OutputEvent::PacketsAvailable); + let packet = Packet::deserialize(packets.into_iter().next().unwrap()).unwrap(); + assert_eq!(&packet.payload[..4], &[0xDE, 0xAD, 0xBE, 0xEF]); + assert_eq!(&packet.payload[4..], &[1, 2, 3, 4, 5]); + assert!(packet.header.extensions.e2ee.is_some()); + } + + #[tokio::test] + async fn test_republish_tracks() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + // Publish a track through the full flow + let track_name: String = Faker.fake(); + let track_sid: DataTrackSid = Faker.fake(); + + let (result_tx, result_rx) = oneshot::channel(); + let event = + PublishRequest { options: DataTrackOptions::new(track_name.clone()), result_tx }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + let handle = event.handle; + + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: handle, + name: track_name.clone(), + uses_e2ee: false, + }; + let event = SfuPublishResponse { handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + + let track = result_rx.await.unwrap().unwrap(); + assert_eq!(track.info().sid(), track_sid); + + // Simulate reconnect + input.send(InputEvent::RepublishTracks).unwrap(); + sleep(Duration::from_millis(50)).await; + + // try_push should fail while republishing + assert!(track.try_push(vec![0xFF].into()).is_err()); + + // SFU re-publishes with a new SID + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + assert_eq!(event.handle, handle); + assert_eq!(event.name, track_name); + + let new_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(new_sid.clone()).into(), + pub_handle: handle, + name: track_name.clone(), + uses_e2ee: false, + }; + let event = SfuPublishResponse { handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + sleep(Duration::from_millis(50)).await; + + // SID updated in place, pushes succeed again + assert_eq!(track.info().sid(), new_sid); + assert!(track.try_push(vec![0xFF].into()).is_ok()); + } + + #[tokio::test] + async fn test_query_published() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + // Publish two tracks + let mut tracks = Vec::new(); + for name in ["track_a", "track_b"] { + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new(name), result_tx }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + let info = DataTrackInfo { + sid: RwLock::new(Faker.fake()).into(), + pub_handle: event.handle, + name: name.into(), + uses_e2ee: false, + }; + let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + + tracks.push(result_rx.await.unwrap().unwrap()); + } + + let published = input.query_tracks().await; + assert_eq!(published.len(), 2); + + let names: Vec<&str> = published.iter().map(|i| i.name()).collect(); + assert!(names.contains(&"track_a")); + assert!(names.contains(&"track_b")); + } + + #[tokio::test] + async fn test_shutdown_with_pending_and_active() { + let options = ManagerOptions { encryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + // Pending publication (no SFU response sent) + let (result_tx, pending_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new("pending"), result_tx }; + input.send(event.into()).unwrap(); + + expect_event!(output, OutputEvent::SfuPublishRequest); + + // Active publication (fully published) + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishRequest { options: DataTrackOptions::new("active"), result_tx }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuPublishRequest); + let info = DataTrackInfo { + sid: RwLock::new(Faker.fake()).into(), + pub_handle: event.handle, + name: "active".into(), + uses_e2ee: false, + }; + let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; + input.send(event.into()).unwrap(); + + let active_track = result_rx.await.unwrap().unwrap(); + assert!(active_track.is_published()); + + // Shutdown the manager + input.send(InputEvent::Shutdown).unwrap(); + sleep(Duration::from_millis(50)).await; + + // Pending publish receives disconnected error + let pending_result = pending_rx.await.unwrap(); + assert!(pending_result.is_err()); + + // Active track is no longer published + assert!(!active_track.is_published()); + } +} diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs new file mode 100644 index 000000000..358547146 --- /dev/null +++ b/livekit-datatrack/src/local/mod.rs @@ -0,0 +1,265 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + api::{DataTrack, DataTrackFrame, DataTrackInfo, InternalError}, + track::DataTrackInner, +}; +use std::{fmt, marker::PhantomData, sync::Arc}; +use thiserror::Error; +use tokio::sync::{mpsc, watch}; + +pub(crate) mod events; +pub(crate) mod manager; +pub(crate) mod proto; + +mod packetizer; +mod pipeline; + +/// Data track published by the local participant. +pub type LocalDataTrack = DataTrack; + +/// Marker type indicating a [`DataTrack`] belongs to the local participant. +/// +/// See also: [`LocalDataTrack`] +/// +#[derive(Debug, Clone)] +pub struct Local; + +impl DataTrack { + pub(crate) fn new(info: Arc, inner: LocalTrackInner) -> Self { + Self { info, inner: Arc::new(inner.into()), _location: PhantomData } + } + + fn inner(&self) -> &LocalTrackInner { + match &*self.inner { + DataTrackInner::Local(track) => track, + DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) + } + } +} + +impl DataTrack { + /// Try pushing a frame to subscribers of the track. + /// + /// # Example + /// + /// ``` + /// # use livekit_datatrack::api::{LocalDataTrack, DataTrackFrame, PushFrameError}; + /// # fn example(track: LocalDataTrack) -> Result<(), PushFrameError> { + /// fn read_sensor() -> Vec { + /// // Read some sensor data... + /// vec![0xFA; 16] + /// } + /// + /// let frame = read_sensor().into(); // Convert to frame + /// track.try_push(frame)?; + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// See [`DataTrackFrame`] for more ways to construct a frame and how to attach metadata. + /// + /// # Errors + /// + /// Pushing a frame can fail for several reasons: + /// + /// - The track has been unpublished by the local participant or SFU + /// - The room is no longer connected + /// - Frames are being pushed too fast + /// + pub fn try_push(&self, frame: DataTrackFrame) -> Result<(), PushFrameError> { + match self.inner().publish_state() { + manager::PublishState::Republishing => { + return Err(PushFrameError::new(frame, PushFrameErrorReason::QueueFull))? + } + manager::PublishState::Unpublished => { + return Err(PushFrameError::new(frame, PushFrameErrorReason::TrackUnpublished))?; + } + manager::PublishState::Published => {} + } + self.inner() + .frame_tx + .try_send(frame) + .map_err(|err| PushFrameError::new(err.into_inner(), PushFrameErrorReason::QueueFull)) + } + + /// Unpublishes the track. + pub fn unpublish(&self) { + self.inner().local_unpublish(); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct LocalTrackInner { + pub frame_tx: mpsc::Sender, + pub state_tx: watch::Sender, +} + +impl LocalTrackInner { + fn publish_state(&self) -> manager::PublishState { + *self.state_tx.borrow() + } + + pub(crate) fn is_published(&self) -> bool { + // Note: a track which is internally in the "resubscribing" state + // is still considered published from the public API perspective. + self.publish_state() != manager::PublishState::Unpublished + } + + pub(crate) async fn wait_for_unpublish(&self) { + _ = self + .state_tx + .subscribe() + .wait_for(|state| *state == manager::PublishState::Unpublished) + .await + } + + fn local_unpublish(&self) { + _ = self.state_tx.send(manager::PublishState::Unpublished); + } +} + +impl Drop for LocalTrackInner { + fn drop(&mut self) { + // Implicit unpublish when handle dropped. + self.local_unpublish(); + } +} + +/// Options for publishing a data track. +/// +/// # Examples +/// +/// Create options for publishing a track named "my_track": +/// +/// ``` +/// # use livekit_datatrack::api::DataTrackOptions; +/// let options = DataTrackOptions::new("my_track"); +/// ``` +/// +#[derive(Clone, Debug)] +pub struct DataTrackOptions { + pub(crate) name: String, +} + +impl DataTrackOptions { + /// Creates options with the given track name. + /// + /// The track name is used to identify the track to other participants. + /// + /// # Requirements + /// - Must not be empty + /// - Must be unique per publisher + /// + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + +impl From for DataTrackOptions { + fn from(name: String) -> Self { + Self::new(name) + } +} + +impl From<&str> for DataTrackOptions { + fn from(name: &str) -> Self { + Self::new(name.to_string()) + } +} + +/// An error that can occur when publishing a data track. +#[derive(Debug, Error)] +pub enum PublishError { + /// Local participant does not have permission to publish data tracks. + /// + /// Ensure the participant's token contains the `canPublishData` grant. + /// + #[error("Data track publishing unauthorized")] + NotAllowed, + + /// A track with the same name is already published by the local participant. + #[error("Track name already taken")] + DuplicateName, + + /// The track name is invalid. + /// + /// This occurs when the name is empty or exceeds the allowed maximum length. + /// + #[error("Track name invalid")] + InvalidName, + + /// Request to publish the track took long to complete. + #[error("Publish data track timed-out")] + Timeout, + + /// No additional data tracks can be published by the local participant. + #[error("Data track publication limit reached")] + LimitReached, + + /// Cannot publish data track when the room is disconnected. + #[error("Room disconnected")] + Disconnected, + + /// Internal error, please report on GitHub. + #[error(transparent)] + Internal(#[from] InternalError), +} + +/// Frame could not be pushed to a data track. +#[derive(Debug, Error)] +#[error("Failed to publish frame: {reason}")] +pub struct PushFrameError { + frame: DataTrackFrame, + reason: PushFrameErrorReason, +} + +impl PushFrameError { + pub(crate) fn new(frame: DataTrackFrame, reason: PushFrameErrorReason) -> Self { + Self { frame, reason } + } + + /// Returns the reason the frame could not be pushed. + pub fn reason(&self) -> PushFrameErrorReason { + self.reason + } + + /// Consumes the error and returns the frame that couldn't be pushed. + /// + /// This may be useful for implementing application-specific retry logic. + /// + pub fn into_frame(self) -> DataTrackFrame { + self.frame + } +} + +/// Reason why a data track frame could not be pushed. +#[derive(Debug, Clone, Copy)] +pub enum PushFrameErrorReason { + /// Track is no longer published. + TrackUnpublished, + /// Frame was dropped due to the pipeline queue being full. + QueueFull, +} + +impl fmt::Display for PushFrameErrorReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TrackUnpublished => write!(f, "track unpublished"), + Self::QueueFull => write!(f, "queue full"), + } + } +} diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs new file mode 100644 index 000000000..63e138b61 --- /dev/null +++ b/livekit-datatrack/src/local/packetizer.rs @@ -0,0 +1,146 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + packet::{Clock, Extensions, FrameMarker, Handle, Header, Packet, Timestamp}, + utils::{BytesChunkExt, Counter}, +}; +use bytes::Bytes; +use thiserror::Error; + +/// Converts application-level frames into packets for transport. +#[derive(Debug)] +pub struct Packetizer { + handle: Handle, + mtu_size: usize, + sequence: Counter, + frame_number: Counter, + clock: Clock<90_000>, +} + +/// Frame packetized by [`Packetizer`]. +pub struct PacketizerFrame { + pub payload: Bytes, + pub extensions: Extensions, +} + +#[derive(Error, Debug)] +pub enum PacketizerError { + #[error("MTU is too short to send frame")] + MtuTooShort, +} + +impl Packetizer { + /// Creates a new packetizer. + pub fn new(track_handle: Handle, mtu_size: usize) -> Self { + Self { + handle: track_handle, + mtu_size, + sequence: Default::default(), + frame_number: Default::default(), + clock: Clock::new(Timestamp::random()), + } + } + + /// Packetizes a frame into one or more packets. + pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { + let mut header = Header { + marker: FrameMarker::Inter, + track_handle: self.handle, + sequence: 0, + frame_number: 0, + timestamp: self.clock.now(), + extensions: frame.extensions, + }; + let max_payload_size = self.mtu_size.saturating_sub(header.serialized_len()); + if max_payload_size == 0 { + Err(PacketizerError::MtuTooShort)? + } + header.frame_number = self.frame_number.get_then_increment(); + + let packet_count = frame.payload.len().div_ceil(max_payload_size); + let packets = frame + .payload + .into_chunks(max_payload_size) + .enumerate() + .map(|(index, payload)| Packet { + header: Header { + marker: Self::frame_marker(index, packet_count), + sequence: self.sequence.get_then_increment(), + extensions: header.extensions.clone(), + ..header + }, + payload, + }) + .collect(); + Ok(packets) + } + + fn frame_marker(index: usize, packet_count: usize) -> FrameMarker { + if packet_count <= 1 { + return FrameMarker::Single; + } + match index { + 0 => FrameMarker::Start, + _ if index == packet_count - 1 => FrameMarker::Final, + _ => FrameMarker::Inter, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::Handle; + use fake::{Fake, Faker}; + use test_case::test_case; + + #[test_case(0, 1, FrameMarker::Single)] + #[test_case(0, 10, FrameMarker::Start)] + #[test_case(4, 10, FrameMarker::Inter)] + #[test_case(9, 10, FrameMarker::Final)] + fn test_frame_marker(index: usize, packet_count: usize, expected_marker: FrameMarker) { + assert_eq!(Packetizer::frame_marker(index, packet_count), expected_marker); + } + + #[test_case(0, 1_024 ; "zero_payload")] + #[test_case(128, 1_024 ; "single_packet")] + #[test_case(20_480, 1_024 ; "multi_packet")] + #[test_case(40_960, 16_000 ; "multi_packet_mtu_16000")] + fn test_packetize(payload_size: usize, mtu_size: usize) { + let handle: Handle = Faker.fake(); + let extensions: Extensions = Faker.fake(); + + let mut packetizer = Packetizer::new(handle, mtu_size); + + let frame = PacketizerFrame { + payload: Bytes::from(vec![0xAB; payload_size]), + extensions: extensions.clone(), + }; + let packets = packetizer.packetize(frame).expect("Failed to packetize"); + + if packets.len() == 0 { + assert_eq!(payload_size, 0, "Should be no packets for zero payload"); + return; + } + + for (index, packet) in packets.iter().enumerate() { + assert_eq!(packet.header.marker, Packetizer::frame_marker(index, packets.len())); + assert_eq!(packet.header.frame_number, 0); + assert_eq!(packet.header.track_handle, handle); + assert_eq!(packet.header.sequence, index as u16); + assert_eq!(packet.header.extensions, extensions); + } + } +} diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs new file mode 100644 index 000000000..9cacf69dc --- /dev/null +++ b/livekit-datatrack/src/local/pipeline.rs @@ -0,0 +1,121 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::packetizer::{Packetizer, PacketizerFrame}; +use crate::{ + api::{DataTrackFrame, DataTrackInfo}, + e2ee::{EncryptionError, EncryptionProvider}, + local::packetizer::PacketizerError, + packet::{self, Extensions, Packet, UserTimestampExt}, +}; +use from_variants::FromVariants; +use std::sync::Arc; +use thiserror::Error; +/// Options for creating a [`Pipeline`]. +pub(super) struct PipelineOptions { + pub info: Arc, + pub encryption_provider: Option>, +} + +/// Pipeline for an individual published data track. +pub(super) struct Pipeline { + encryption_provider: Option>, + packetizer: Packetizer, +} + +#[derive(Debug, Error, FromVariants)] +pub(super) enum PipelineError { + #[error(transparent)] + Packetizer(PacketizerError), + #[error(transparent)] + Encryption(EncryptionError), +} + +impl Pipeline { + /// Creates a new pipeline with the given options. + pub fn new(options: PipelineOptions) -> Self { + debug_assert_eq!(options.info.uses_e2ee, options.encryption_provider.is_some()); + let packetizer = Packetizer::new(options.info.pub_handle, Self::TRANSPORT_MTU); + Self { encryption_provider: options.encryption_provider, packetizer } + } + + pub fn process_frame(&mut self, frame: DataTrackFrame) -> Result, PipelineError> { + let frame = self.encrypt_if_needed(frame.into())?; + let packets = self.packetizer.packetize(frame)?; + Ok(packets) + } + + /// Encrypt the frame's payload if E2EE is enabled for this track. + fn encrypt_if_needed( + &self, + mut frame: PacketizerFrame, + ) -> Result { + let Some(e2ee_provider) = &self.encryption_provider else { + return Ok(frame); + }; + + let encrypted = e2ee_provider.encrypt(frame.payload)?; + + frame.payload = encrypted.payload; + frame.extensions.e2ee = + packet::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); + Ok(frame) + } + + /// Maximum transmission unit (MTU) of the transport. + const TRANSPORT_MTU: usize = 16_000; +} + +impl From for PacketizerFrame { + fn from(frame: DataTrackFrame) -> Self { + Self { + payload: frame.payload, + extensions: Extensions { + user_timestamp: frame.user_timestamp.map(UserTimestampExt), + e2ee: None, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use fake::{Fake, Faker}; + + #[test] + fn test_process_frame() { + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let options = PipelineOptions { info: info.into(), encryption_provider: None }; + let mut pipeline = Pipeline::new(options); + + let repeated_byte: u8 = Faker.fake(); + let frame = DataTrackFrame { + payload: Bytes::from(vec![repeated_byte; 32_000]), + user_timestamp: Faker.fake(), + }; + + let packets = pipeline.process_frame(frame).unwrap(); + assert_eq!(packets.len(), 3); + + for packet in packets { + assert!(packet.header.extensions.e2ee.is_none()); + assert!(!packet.payload.is_empty()); + assert!(packet.payload.iter().all(|byte| *byte == repeated_byte)); + } + } +} diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs new file mode 100644 index 000000000..bb891bec4 --- /dev/null +++ b/livekit-datatrack/src/local/proto.rs @@ -0,0 +1,205 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::events::*; +use crate::{ + api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, + packet::Handle, +}; +use anyhow::{anyhow, Context}; +use livekit_protocol as proto; +use std::{borrow::Borrow, sync::RwLock}; + +// MARK: - Output event -> protocol + +impl From for proto::PublishDataTrackRequest { + fn from(event: SfuPublishRequest) -> Self { + use proto::encryption::Type; + let encryption = if event.uses_e2ee { Type::Gcm } else { Type::None }.into(); + Self { pub_handle: event.handle.into(), name: event.name, encryption } + } +} + +impl From for proto::UnpublishDataTrackRequest { + fn from(event: SfuUnpublishRequest) -> Self { + Self { pub_handle: event.handle.into() } + } +} + +// MARK: - Protocol -> input event + +impl TryFrom for SfuPublishResponse { + type Error = InternalError; + + fn try_from(msg: proto::PublishDataTrackResponse) -> Result { + let info: DataTrackInfo = msg.info.context("Missing info")?.try_into()?; + Ok(Self { handle: info.pub_handle, result: Ok(info) }) + } +} + +impl TryFrom for SfuUnpublishResponse { + type Error = InternalError; + + fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { + let handle: Handle = + msg.info.context("Missing info")?.pub_handle.try_into().map_err(anyhow::Error::from)?; + Ok(Self { handle }) + } +} + +impl TryFrom for DataTrackInfo { + type Error = InternalError; + + fn try_from(msg: proto::DataTrackInfo) -> Result { + let handle: Handle = msg.pub_handle.try_into().map_err(anyhow::Error::from)?; + let uses_e2ee = match msg.encryption() { + proto::encryption::Type::None => false, + proto::encryption::Type::Gcm => true, + other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, + }; + let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?; + Ok(Self { pub_handle: handle, sid: RwLock::new(sid).into(), name: msg.name, uses_e2ee }) + } +} + +pub fn publish_result_from_request_response( + msg: &proto::RequestResponse, +) -> Option { + use proto::request_response::{Reason, Request}; + let Some(request) = &msg.request else { return None }; + let Request::PublishDataTrack(request) = request else { return None }; + let Ok(handle) = TryInto::::try_into(request.pub_handle) else { return None }; + let error = match msg.reason() { + // If new error reasons are introduced in the future, consider adding them + // to the public error enum if they are useful to the user. + Reason::NotAllowed => PublishError::NotAllowed, + Reason::DuplicateName => PublishError::DuplicateName, + Reason::InvalidName => PublishError::InvalidName, + _ => PublishError::Internal(anyhow!("SFU rejected: {}", msg.message).into()), + }; + let event = SfuPublishResponse { handle, result: Err(error) }; + Some(event) +} + +// MARK: - Sync state support + +impl From for proto::DataTrackInfo { + fn from(info: DataTrackInfo) -> Self { + let encryption = if info.uses_e2ee() { + proto::encryption::Type::Gcm + } else { + proto::encryption::Type::None + }; + Self { + pub_handle: info.pub_handle.into(), + sid: info.sid().to_string(), + name: info.name, + encryption: encryption as i32, + } + } +} + +/// Form publish responses for each publish data track to support sync state. +pub fn publish_responses_for_sync_state( + published_tracks: impl IntoIterator>, +) -> Vec { + published_tracks + .into_iter() + .map(|info| proto::PublishDataTrackResponse { info: Some(info.borrow().clone().into()) }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; + + #[test] + fn test_from_publish_request_event() { + let event = SfuPublishRequest { + handle: 1u32.try_into().unwrap(), + name: "track".into(), + uses_e2ee: true, + }; + let request: proto::PublishDataTrackRequest = event.into(); + assert_eq!(request.pub_handle, 1); + assert_eq!(request.name, "track"); + assert_eq!(request.encryption(), proto::encryption::Type::Gcm); + } + + #[test] + fn test_from_unpublish_request_event() { + let event = SfuUnpublishRequest { handle: 1u32.try_into().unwrap() }; + let request: proto::UnpublishDataTrackRequest = event.into(); + assert_eq!(request.pub_handle, 1); + } + + #[test] + fn test_from_publish_response() { + let response = proto::PublishDataTrackResponse { + info: proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track".into(), + encryption: proto::encryption::Type::Gcm.into(), + } + .into(), + }; + let event: SfuPublishResponse = response.try_into().unwrap(); + assert_eq!(event.handle, 1u32.try_into().unwrap()); + + let info = event.result.expect("Expected ok result"); + assert_eq!(info.pub_handle, 1u32.try_into().unwrap()); + assert_eq!(*info.sid.read().unwrap(), "DTR_1234".to_string().try_into().unwrap()); + assert_eq!(info.name, "track"); + assert!(info.uses_e2ee); + } + + #[test] + fn test_from_request_response() { + use proto::request_response::{Reason, Request}; + let response = proto::RequestResponse { + request: Request::PublishDataTrack(proto::PublishDataTrackRequest { + pub_handle: 1, + ..Default::default() + }) + .into(), + reason: Reason::NotAllowed.into(), + ..Default::default() + }; + + let event = publish_result_from_request_response(&response).expect("Expected event"); + assert_eq!(event.handle, 1u32.try_into().unwrap()); + assert!(matches!(event.result, Err(PublishError::NotAllowed))); + } + + #[test] + fn test_publish_responses_for_sync_state() { + let mut first: DataTrackInfo = Faker.fake(); + first.uses_e2ee = true; + + let mut second: DataTrackInfo = Faker.fake(); + second.uses_e2ee = false; + + let publish_responses = publish_responses_for_sync_state(vec![first, second]); + assert_eq!( + publish_responses[0].info.as_ref().unwrap().encryption(), + proto::encryption::Type::Gcm + ); + assert_eq!( + publish_responses[1].info.as_ref().unwrap().encryption(), + proto::encryption::Type::None + ); + } +} diff --git a/livekit-datatrack/src/packet/deserialize.rs b/livekit-datatrack/src/packet/deserialize.rs new file mode 100644 index 000000000..ecb471132 --- /dev/null +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -0,0 +1,331 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + consts::*, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, Packet, + Timestamp, UserTimestampExt, +}; +use bytes::{Buf, Bytes}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DeserializeError { + #[error("too short to contain a valid header")] + TooShort, + + #[error("header exceeds total packet length")] + HeaderOverrun, + + #[error("extension word indicator is missing")] + MissingExtWords, + + #[error("unsupported version {0}")] + UnsupportedVersion(u8), + + #[error("invalid track handle: {0}")] + InvalidHandle(#[from] HandleError), + + #[error("extension with tag {0} is malformed")] + MalformedExt(ExtensionTag), +} + +impl Packet { + pub fn deserialize(mut raw: Bytes) -> Result { + let header = Header::deserialize(&mut raw)?; + let payload_len = raw.remaining(); + let payload = raw.copy_to_bytes(payload_len); + Ok(Self { header, payload }) + } +} + +impl Header { + fn deserialize(raw: &mut impl Buf) -> Result { + if raw.remaining() < BASE_HEADER_LEN { + Err(DeserializeError::TooShort)? + } + let initial = raw.get_u8(); + + let version = initial >> VERSION_SHIFT & VERSION_MASK; + if version > SUPPORTED_VERSION { + Err(DeserializeError::UnsupportedVersion(version))? + } + let marker = match initial >> FRAME_MARKER_SHIFT & FRAME_MARKER_MASK { + FRAME_MARKER_START => FrameMarker::Start, + FRAME_MARKER_FINAL => FrameMarker::Final, + FRAME_MARKER_SINGLE => FrameMarker::Single, + _ => FrameMarker::Inter, + }; + let ext_flag = (initial >> EXT_FLAG_SHIFT & EXT_FLAG_MASK) > 0; + raw.advance(1); // Reserved + + let track_handle: Handle = raw.get_u16().try_into()?; + let sequence = raw.get_u16(); + let frame_number = raw.get_u16(); + let timestamp = Timestamp::from_ticks(raw.get_u32()); + + let mut extensions = Extensions::default(); + if ext_flag { + if raw.remaining() < 2 { + Err(DeserializeError::MissingExtWords)?; + } + let ext_words = raw.get_u16(); + + let ext_len = 4 * (ext_words as usize + 1); + if ext_len > raw.remaining() { + Err(DeserializeError::HeaderOverrun)? + } + let ext_block = raw.copy_to_bytes(ext_len); + extensions = Extensions::deserialize(ext_block)?; + } + + let header = Header { marker, track_handle, sequence, frame_number, timestamp, extensions }; + Ok(header) + } +} + +macro_rules! deserialize_ext { + ($ext_type:ty, $raw:expr, $len:expr) => {{ + if $raw.remaining() < $len { + Err(DeserializeError::MalformedExt(<$ext_type>::TAG))? + } + let mut buf = [0u8; <$ext_type>::LEN]; + $raw.copy_to_slice(&mut buf); + + let extra_bytes = $len - <$ext_type>::LEN; + if extra_bytes > 0 { + // Extra bytes, possibly from future extension version (skip) + $raw.advance(extra_bytes); + } + Some(<$ext_type>::deserialize(buf)) + }}; +} + +impl Extensions { + fn deserialize(mut raw: impl Buf) -> Result { + let mut extensions = Self::default(); + while raw.remaining() >= 2 * size_of::() { + let tag = raw.get_u8(); + let len = raw.get_u8() as usize; + match tag { + EXT_TAG_PADDING => {} // Skip padding + E2eeExt::TAG if len >= E2eeExt::LEN => { + extensions.e2ee = deserialize_ext!(E2eeExt, raw, len); + } + UserTimestampExt::TAG if len >= UserTimestampExt::LEN => { + extensions.user_timestamp = deserialize_ext!(UserTimestampExt, raw, len); + } + _ => { + // Skip over unknown or length-mismatched extensions (forward compatible). + if raw.remaining() < len { + Err(DeserializeError::MalformedExt(tag))? + } + raw.advance(len); + continue; + } + } + } + Ok(extensions) + } +} + +impl UserTimestampExt { + fn deserialize(raw: [u8; Self::LEN]) -> Self { + let timestamp = u64::from_be_bytes(raw); + Self(timestamp) + } +} + +impl E2eeExt { + fn deserialize(raw: [u8; Self::LEN]) -> Self { + let key_index = raw[0]; + let mut iv = [0u8; 12]; + iv.copy_from_slice(&raw[1..13]); + Self { key_index, iv } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::{BufMut, BytesMut}; + use test_case::test_matrix; + + /// Returns the simplest valid packet to use in test. + fn valid_packet() -> BytesMut { + let mut raw = BytesMut::zeroed(12); // Base header + raw[3] = 1; // Non-zero track handle + raw + } + + #[test] + fn test_short_buffer() { + let mut raw = valid_packet(); + raw.truncate(11); + + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::TooShort))); + } + + #[test] + fn test_missing_ext_words() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + // Should have ext word indicator here + + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::MissingExtWords))); + } + + #[test] + fn test_header_overrun() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(1); // One extension word + + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::HeaderOverrun))); + } + + #[test] + fn test_unsupported_version() { + let mut raw = valid_packet(); + raw[0] = 0x20; // Version 1 (not supported yet) + + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::UnsupportedVersion(1)))); + } + + #[test] + fn test_base_header() { + let mut raw = BytesMut::new(); + raw.put_u8(0x8); // Version 0, final flag set, no extensions + raw.put_u8(0x0); // Reserved + raw.put_slice(&[0x88, 0x11]); // Track ID + raw.put_slice(&[0x44, 0x22]); // Sequence + raw.put_slice(&[0x44, 0x11]); // Frame number + raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!(packet.header.marker, FrameMarker::Final); + assert_eq!(packet.header.track_handle, 0x8811u32.try_into().unwrap()); + assert_eq!(packet.header.sequence, 0x4422); + assert_eq!(packet.header.frame_number, 0x4411); + assert_eq!(packet.header.timestamp, Timestamp::from_ticks(0x44221188)); + assert_eq!(packet.header.extensions.user_timestamp, None); + assert_eq!(packet.header.extensions.e2ee, None); + } + + #[test_matrix([0, 1, 24])] + fn test_ext_skips_padding(ext_words: usize) { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + + raw.put_u16(ext_words as u16); // Extension words + raw.put_bytes(0, (ext_words + 1) * 4); // Padding + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!(packet.payload.len(), 0); + } + + #[test] + fn test_ext_e2ee() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(3); // Extension words + + raw.put_u8(1); // ID 1 + raw.put_u8(13); // Length + raw.put_u8(0xFA); // Key index + raw.put_bytes(0x3C, 12); // IV + raw.put_bytes(0, 1); // Padding + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + let e2ee = packet.header.extensions.e2ee.unwrap(); + assert_eq!(e2ee.key_index, 0xFA); + assert_eq!(e2ee.iv, [0x3C; 12]); + } + + #[test] + fn test_ext_user_timestamp() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(2); // Extension words + + raw.put_u8(2); + raw.put_u8(8); // Length + raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp + raw.put_bytes(0, 2); // Padding + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!( + packet.header.extensions.user_timestamp, + UserTimestampExt(0x4411221111118811).into() + ); + } + + #[test] + fn test_ext_forward_compat_longer_length() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(3); // Extension words + + raw.put_u8(2); // User timestamp + raw.put_u8(12); // Longer than known length (8), extra bytes are skipped + raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // Known 8 bytes + raw.put_bytes(0xFF, 4); // 4 extra bytes from a future version + raw.put_bytes(0, 2); // Padding + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!( + packet.header.extensions.user_timestamp, + UserTimestampExt(0x4411221111118811).into() + ); + } + + #[test] + fn test_ext_shorter_than_known_length_skipped() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(1); // Extension words + + raw.put_u8(2); // User timestamp tag + raw.put_u8(4); // Shorter than known length (8), treated as unknown + raw.put_bytes(0x3C, 4); + raw.put_bytes(0, 2); // Padding + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert!(packet.header.extensions.user_timestamp.is_none()); + } + + #[test] + fn test_ext_unknown() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(0); // Extension words + + raw.put_u8(8); // ID 8 (unknown) + raw.put_bytes(0, 7); + Packet::deserialize(raw.freeze()).expect("Should skip unknown extension"); + } + + #[test] + fn test_ext_required_word_alignment() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(0); // Extension words + raw.put_bytes(0, 3); // Padding, missing one byte + + assert!(Packet::deserialize(raw.freeze()).is_err()); + } +} diff --git a/livekit-datatrack/src/packet/extension.rs b/livekit-datatrack/src/packet/extension.rs new file mode 100644 index 000000000..a41f9d7de --- /dev/null +++ b/livekit-datatrack/src/packet/extension.rs @@ -0,0 +1,52 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::fmt; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct Extensions { + pub user_timestamp: Option, + pub e2ee: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct UserTimestampExt(pub u64); + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct E2eeExt { + pub key_index: u8, + pub iv: [u8; 12], +} + +impl fmt::Debug for E2eeExt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // For security, do not include fields in debug. + f.debug_struct("E2ee").finish() + } +} + +pub(super) type ExtensionTag = u8; + +impl UserTimestampExt { + pub(super) const TAG: ExtensionTag = 2; + pub(super) const LEN: usize = 8; +} + +impl E2eeExt { + pub(super) const TAG: ExtensionTag = 1; + pub(super) const LEN: usize = 13; +} diff --git a/livekit-datatrack/src/packet/handle.rs b/livekit-datatrack/src/packet/handle.rs new file mode 100644 index 000000000..0913a0529 --- /dev/null +++ b/livekit-datatrack/src/packet/handle.rs @@ -0,0 +1,93 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Display; +use thiserror::Error; + +/// Value identifying which data track a packet belongs to. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Handle(u16); + +#[derive(Debug, Error)] +pub enum HandleError { + #[error("{0:#X} is reserved")] + Reserved(u16), + + #[error("value too large to be a valid track handle")] + TooLarge, +} + +impl TryFrom for Handle { + type Error = HandleError; + + fn try_from(value: u16) -> Result { + if value == 0 { + Err(HandleError::Reserved(value))? + } + Ok(Self(value)) + } +} + +impl TryFrom for Handle { + type Error = HandleError; + + fn try_from(value: u32) -> Result { + let value: u16 = value.try_into().map_err(|_| HandleError::TooLarge)?; + value.try_into() + } +} + +impl From for u16 { + fn from(handle: Handle) -> Self { + handle.0 + } +} + +impl From for u32 { + fn from(handle: Handle) -> Self { + handle.0 as u32 + } +} + +impl Display for Handle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{0:#X}", self.0) + } +} + +/// Utility for allocating unique track handles to use for publishing tracks. +/// +/// This is currently a simple linear allocator and handles are not reused +/// once being allocated. +/// +#[derive(Debug, Default)] +pub struct HandleAllocator { + /// Next handle value. + value: u16, +} + +impl HandleAllocator { + /// Returns a unique track handle for the next publication, if one can be obtained. + pub fn get(&mut self) -> Option { + self.value = self.value.checked_add(1)?; + Handle(self.value).into() + } +} + +#[cfg(test)] +impl fake::Dummy for Handle { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + Self::try_from(rng.random_range(1..u16::MAX)).unwrap() + } +} diff --git a/livekit-datatrack/src/packet/mod.rs b/livekit-datatrack/src/packet/mod.rs new file mode 100644 index 000000000..86b5af5fe --- /dev/null +++ b/livekit-datatrack/src/packet/mod.rs @@ -0,0 +1,110 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::Bytes; +use core::fmt; + +mod deserialize; +mod extension; +mod handle; +mod serialize; +mod time; + +pub use extension::*; +pub use handle::*; +pub use time::*; + +#[derive(Clone)] +pub struct Packet { + pub header: Header, + pub payload: Bytes, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct Header { + pub marker: FrameMarker, + pub track_handle: Handle, + pub sequence: u16, + pub frame_number: u16, + pub timestamp: Timestamp<90_000>, + pub extensions: Extensions, +} + +/// Marker indicating a packet's position in relation to a frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub enum FrameMarker { + /// Packet is the first in a frame. + Start, + /// Packet is within a frame. + Inter, + /// Packet is the last in a frame. + Final, + /// Packet is the only one in a frame. + Single, +} + +impl fmt::Debug for Packet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Packet") + .field("header", &self.header) + .field("payload_len", &self.payload.len()) + .finish() + } +} + +/// Constants used for serialization and deserialization. +pub(crate) mod consts { + pub const SUPPORTED_VERSION: u8 = 0; + pub const BASE_HEADER_LEN: usize = 12; + + // Bitfield shifts and masks for header flags + pub const VERSION_SHIFT: u8 = 5; + pub const VERSION_MASK: u8 = 0x07; + + pub const FRAME_MARKER_SHIFT: u8 = 3; + pub const FRAME_MARKER_MASK: u8 = 0x3; + + pub const FRAME_MARKER_START: u8 = 0x2; + pub const FRAME_MARKER_FINAL: u8 = 0x1; + pub const FRAME_MARKER_INTER: u8 = 0x0; + pub const FRAME_MARKER_SINGLE: u8 = 0x3; + + pub const EXT_WORDS_INDICATOR_SIZE: usize = 2; + pub const EXT_FLAG_SHIFT: u8 = 0x2; + pub const EXT_FLAG_MASK: u8 = 0x1; + pub const EXT_MARKER_LEN: usize = 2; + pub const EXT_TAG_PADDING: u8 = 0; +} + +#[cfg(test)] +mod tests { + use super::Packet; + use fake::{Fake, Faker}; + + #[test] + fn test_roundtrip() { + let original: Packet = Faker.fake(); + + let header = original.header.clone(); + let payload = original.payload.clone(); + + let serialized = original.serialize(); + let deserialized = Packet::deserialize(serialized).unwrap(); + + assert_eq!(deserialized.header, header); + assert_eq!(deserialized.payload, payload); + } +} diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs new file mode 100644 index 000000000..99fda1f3c --- /dev/null +++ b/livekit-datatrack/src/packet/serialize.rs @@ -0,0 +1,242 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{consts::*, E2eeExt, Extensions, FrameMarker, Header, Packet, UserTimestampExt}; +use bytes::{BufMut, Bytes, BytesMut}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SerializeError { + #[error("buffer cannot fit header")] + TooSmallForHeader, + + #[error("buffer cannot fit payload")] + TooSmallForPayload, +} + +impl Packet { + /// Length of the serialized packet in bytes. + pub fn serialized_len(&self) -> usize { + self.header.serialized_len() + self.payload.len() + } + + /// Serialize the packet into the given buffer. + /// + /// If the given buffer is too short to accommodate the serialized packet, the result + /// is an error. Use [`Self::serialized_len()`] to get the required buffer size. + /// + pub fn serialize_into(self, buf: &mut impl BufMut) -> Result { + let payload_len = self.payload.len(); + let header_len = self.header.serialize_into(buf)?; + if buf.remaining_mut() < payload_len { + Err(SerializeError::TooSmallForPayload)? + } + buf.put(self.payload); + Ok(header_len + payload_len) + } + + /// Serialize the packet into a new buffer. + pub fn serialize(self) -> Bytes { + let len = self.serialized_len(); + let mut buf = BytesMut::with_capacity(len); + + let written = self.serialize_into(&mut buf).unwrap(); + assert_eq!(written, len); + buf.freeze() + } +} + +struct HeaderMetrics { + ext_len: usize, + ext_words: usize, + padding_len: usize, +} + +impl HeaderMetrics { + fn serialized_len(&self) -> usize { + let mut len = BASE_HEADER_LEN; + if self.ext_len > 0 { + len += EXT_WORDS_INDICATOR_SIZE + self.ext_len + self.padding_len; + } + len + } +} + +impl Header { + /// Lengths of individual elements in the serialized header. + fn metrics(&self) -> HeaderMetrics { + let ext_len = self.extensions.serialized_len(); + let ext_words = ext_len.div_ceil(4); + let padding_len = (ext_words * 4) - ext_len; + HeaderMetrics { ext_len, ext_words, padding_len } + } + + /// Length of the serialized header in bytes. + pub fn serialized_len(&self) -> usize { + self.metrics().serialized_len() + } + + fn serialize_into(self, buf: &mut impl BufMut) -> Result { + let metrics = self.metrics(); + let serialized_len = metrics.serialized_len(); + let remaining_initial = buf.remaining_mut(); + + if buf.remaining_mut() < serialized_len { + Err(SerializeError::TooSmallForHeader)? + } + + let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; + let marker = match self.marker { + FrameMarker::Single => FRAME_MARKER_SINGLE, + FrameMarker::Start => FRAME_MARKER_START, + FrameMarker::Inter => FRAME_MARKER_INTER, + FrameMarker::Final => FRAME_MARKER_FINAL, + }; + initial |= marker << FRAME_MARKER_SHIFT; + + if metrics.ext_len > 0 { + initial |= 1 << EXT_FLAG_SHIFT; + } + buf.put_u8(initial); + buf.put_u8(0); // Reserved + + buf.put_u16(self.track_handle.into()); + buf.put_u16(self.sequence); + buf.put_u16(self.frame_number); + buf.put_u32(self.timestamp.as_ticks()); + + if metrics.ext_len > 0 { + // Extension words are encoded as count - 1 as per spec + buf.put_u16((metrics.ext_words - 1) as u16); + self.extensions.serialize_into(buf); + buf.put_bytes(0, metrics.padding_len); + } + + assert_eq!(remaining_initial - buf.remaining_mut(), serialized_len); + Ok(serialized_len) + } +} + +impl Extensions { + /// Length of extensions excluding padding. + fn serialized_len(&self) -> usize { + let mut len = 0; + if self.e2ee.is_some() { + len += EXT_MARKER_LEN + E2eeExt::LEN; + } + if self.user_timestamp.is_some() { + len += EXT_MARKER_LEN + UserTimestampExt::LEN; + } + len + } + + fn serialize_into(self, buf: &mut impl BufMut) { + if let Some(e2ee) = self.e2ee { + e2ee.serialize_into(buf); + } + if let Some(user_timestamp) = self.user_timestamp { + user_timestamp.serialize_into(buf); + } + } +} + +impl E2eeExt { + fn serialize_into(self, buf: &mut impl BufMut) { + buf.put_u8(Self::TAG); + buf.put_u8(Self::LEN as u8); + buf.put_u8(self.key_index); + buf.put_slice(&self.iv); + } +} + +impl UserTimestampExt { + fn serialize_into(self, buf: &mut impl BufMut) { + buf.put_u8(Self::TAG); + buf.put_u8(Self::LEN as u8); + buf.put_u64(self.0); + } +} + +#[cfg(test)] +mod tests { + use crate::packet::{ + E2eeExt, Extensions, FrameMarker, Header, Packet, Timestamp, UserTimestampExt, + }; + use bytes::Buf; + + /// Constructed packet to use in tests. + fn packet() -> Packet { + Packet { + header: Header { + marker: FrameMarker::Final, + track_handle: 0x8811u32.try_into().unwrap(), + sequence: 0x4422, + frame_number: 0x4411, + timestamp: Timestamp::from_ticks(0x44221188), + extensions: Extensions { + user_timestamp: UserTimestampExt(0x4411221111118811).into(), + e2ee: E2eeExt { key_index: 0xFA, iv: [0x3C; 12] }.into(), + }, + }, + payload: vec![0xFA; 1024].into(), + } + } + + #[test] + fn test_header_metrics() { + let metrics = packet().header.metrics(); + assert_eq!(metrics.ext_len, 25); + assert_eq!(metrics.ext_words, 7); + assert_eq!(metrics.padding_len, 3); + } + + #[test] + fn test_serialized_length() { + let packet = packet(); + assert_eq!(packet.serialized_len(), 1066); + assert_eq!(packet.header.serialized_len(), 42); + assert_eq!(packet.header.extensions.serialized_len(), 25); + } + + #[test] + fn test_serialize() { + let mut buf = packet().serialize().try_into_mut().unwrap(); + assert_eq!(buf.len(), 1066); + + // Base header + assert_eq!(buf.get_u8(), 0xC); // Version 0, final, extension + assert_eq!(buf.get_u8(), 0); // Reserved + assert_eq!(buf.get_u16(), 0x8811); // Track handle + assert_eq!(buf.get_u16(), 0x4422); // Sequence + assert_eq!(buf.get_u16(), 0x4411); // Frame number + assert_eq!(buf.get_u32(), 0x44221188); // Timestamp + assert_eq!(buf.get_u16(), 6); // Extension words + + // E2EE extension + assert_eq!(buf.get_u8(), 1); // ID 1, + assert_eq!(buf.get_u8(), 13); // Length + assert_eq!(buf.get_u8(), 0xFA); // Key index + assert_eq!(buf.copy_to_bytes(12), vec![0x3C; 12]); + + // User timestamp extension + assert_eq!(buf.get_u8(), 2); // ID 2 + assert_eq!(buf.get_u8(), 8); // Length + assert_eq!(buf.get_u64(), 0x4411221111118811); + + assert_eq!(buf.copy_to_bytes(3), vec![0; 3]); // Padding + assert_eq!(buf.copy_to_bytes(1024), vec![0xFA; 1024]); // Payload + + assert_eq!(buf.remaining(), 0); + } +} diff --git a/livekit-datatrack/src/packet/time.rs b/livekit-datatrack/src/packet/time.rs new file mode 100644 index 000000000..aba02f8d7 --- /dev/null +++ b/livekit-datatrack/src/packet/time.rs @@ -0,0 +1,123 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use rand::Rng; +use std::time::{Duration, Instant}; + +/// Packet-level timestamp. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Timestamp(u32); + +impl Timestamp { + pub fn random() -> Self { + Self::from_ticks(rand::rng().random::()) + } + + pub const fn from_ticks(ticks: u32) -> Self { + Self(ticks) + } + + pub const fn as_ticks(&self) -> u32 { + self.0 + } + + const fn is_before(&self, other: Self) -> bool { + (self.0.wrapping_sub(other.0) as i32) < 0 + } + + const fn wrapping_add(self, ticks: u32) -> Self { + Self(self.0.wrapping_add(ticks)) + } +} + +/// Monotonic mapping from an epoch to a packet-level timestamp. +#[derive(Debug)] +pub struct Clock { + epoch: Instant, + base: Timestamp, + prev: Timestamp, +} + +impl Clock { + /// Creates a new clock with epoch equal to [`Instant::now()`]. + pub fn new(base: Timestamp) -> Self { + Self::with_epoch(Instant::now(), base) + } + + /// Creates a new clock with an explicit epoch instant. + pub fn with_epoch(epoch: Instant, base: Timestamp) -> Self { + Self { epoch, base, prev: base } + } + + /// Returns the timestamp corresponding to [`Instant::now()`]. + pub fn now(&mut self) -> Timestamp { + self.at(Instant::now()) + } + + /// Returns the timestamp corresponding to the given instant. + pub fn at(&mut self, instant: Instant) -> Timestamp { + let elapsed = instant.duration_since(self.epoch); + let ticks = Self::duration_to_ticks(elapsed); + + let mut ts = self.base.wrapping_add(ticks); + // Enforce monotonicity in RTP wraparound space + if ts.is_before(self.prev) { + ts = self.prev; + } + self.prev = ts; + ts + } + + /// Convert a duration since the epoch into clock ticks. + const fn duration_to_ticks(duration: Duration) -> u32 { + // round(nanos * rate_hz / 1e9) + let nanos = duration.as_nanos(); + let ticks = (nanos * RATE as u128 + 500_000_000) / 1_000_000_000; + ticks as u32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + type DefaultClock = Clock<90_000>; + + #[test] + fn test_is_base_at_epoch() { + let epoch = Instant::now(); + let base = Timestamp::from_ticks(1234); + let mut clock = DefaultClock::with_epoch(epoch, base); + + assert_eq!(clock.at(epoch).as_ticks(), base.as_ticks()); + assert_eq!(clock.prev.as_ticks(), base.as_ticks()); + } + + #[test] + fn test_monotonic() { + let epoch = Instant::now(); + let base = Timestamp::from_ticks(0); + let mut clock = DefaultClock::with_epoch(epoch, base); + + let t1 = epoch + Duration::from_millis(100); + let t0 = epoch + Duration::from_millis(50); + assert_eq!(clock.at(t1).as_ticks(), clock.at(t0).as_ticks(), "Clock went backwards"); + } +} + +#[cfg(test)] +impl fake::Dummy for Timestamp { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + Self(rng.random()) + } +} diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs new file mode 100644 index 000000000..af2e456df --- /dev/null +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -0,0 +1,424 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::packet::{Extensions, FrameMarker, Packet}; +use bytes::{Bytes, BytesMut}; +use std::{collections::BTreeMap, fmt::Display}; +use thiserror::Error; + +/// Reassembles packets into frames. +#[derive(Debug)] +pub struct Depacketizer { + /// Partial frame currently being assembled. + partial: Option, +} + +/// A frame that has been fully reassembled by [`Depacketizer`]. +#[derive(Debug)] +pub struct DepacketizerFrame { + pub payload: Bytes, + pub extensions: Extensions, +} + +impl Depacketizer { + /// Maximum number of packets to buffer per frame before dropping. + const MAX_BUFFER_PACKETS: usize = 128; + + /// Creates a new depacketizer. + pub fn new() -> Self { + Self { partial: None } + } + + /// Push a packet into the depacketizer. + pub fn push(&mut self, packet: Packet) -> DepacketizerPushResult { + match packet.header.marker { + FrameMarker::Single => self.frame_from_single(packet), + FrameMarker::Start => self.begin_partial(packet), + FrameMarker::Inter | FrameMarker::Final => self.push_to_partial(packet), + } + } + + fn frame_from_single(&mut self, packet: Packet) -> DepacketizerPushResult { + debug_assert!(packet.header.marker == FrameMarker::Single); + let mut result = DepacketizerPushResult::default(); + if let Some(partial) = self.partial.take() { + result.drop_error = DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); + } + result.frame = + DepacketizerFrame { payload: packet.payload, extensions: packet.header.extensions } + .into(); + result + } + + /// Begin assembling a new packet. + fn begin_partial(&mut self, packet: Packet) -> DepacketizerPushResult { + debug_assert!(packet.header.marker == FrameMarker::Start); + + let mut result = DepacketizerPushResult::default(); + + if let Some(partial) = self.partial.take() { + result.drop_error = DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); + } + + let start_sequence = packet.header.sequence; + let partial = PartialFrame { + frame_number: packet.header.frame_number, + start_sequence, + extensions: packet.header.extensions, + payloads: BTreeMap::from([(start_sequence, packet.payload)]), + }; + self.partial = partial.into(); + + result + } + + /// Push to the existing partial frame. + fn push_to_partial(&mut self, packet: Packet) -> DepacketizerPushResult { + debug_assert!(matches!(packet.header.marker, FrameMarker::Inter | FrameMarker::Final)); + + let Some(mut partial) = self.partial.take() else { + return DepacketizerDropError { + frame_number: packet.header.frame_number, + reason: DepacketizerDropReason::UnknownFrame, + } + .into(); + }; + if packet.header.frame_number != partial.frame_number { + return DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); + } + if partial.payloads.len() >= Self::MAX_BUFFER_PACKETS { + return DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::BufferFull, + } + .into(); + } + + if partial.payloads.insert(packet.header.sequence, packet.payload).is_some() { + log::warn!( + "Duplicate packet for sequence {} on frame {}, replacing with latest", + packet.header.sequence, + partial.frame_number + ); + } + + if packet.header.marker == FrameMarker::Final { + return Self::finalize(partial, packet.header.sequence); + } + + self.partial = Some(partial); + DepacketizerPushResult::default() + } + + /// Try to reassemble the complete frame. + fn finalize(mut partial: PartialFrame, end_sequence: u16) -> DepacketizerPushResult { + let received = partial.payloads.len() as u16; + + let payload_len: usize = partial.payloads.iter().map(|(_, payload)| payload.len()).sum(); + let mut payload = BytesMut::with_capacity(payload_len); + + let mut sequence = partial.start_sequence; + + while let Some(partial_payload) = partial.payloads.remove(&sequence) { + debug_assert!(payload.len() + partial_payload.len() <= payload.capacity()); + payload.extend(partial_payload); + + if sequence != end_sequence { + sequence = sequence.wrapping_add(1); + continue; + } + return DepacketizerFrame { payload: payload.freeze(), extensions: partial.extensions } + .into(); + } + DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Incomplete { + received, + expected: end_sequence.wrapping_sub(partial.start_sequence).wrapping_add(1), + }, + } + .into() + } +} + +/// Frame being assembled as packets are received. +#[derive(Debug)] +struct PartialFrame { + /// Frame number from the start packet. + frame_number: u16, + /// Sequence of the start packet. + start_sequence: u16, + /// Extensions from the start packet. + extensions: Extensions, + /// Mapping between sequence number and packet payload. + payloads: BTreeMap, +} + +/// Result from a call to [`Depacketizer::push`]. +/// +/// The reason this type is used instead of [`core::result::Result`] is due to the fact a single +/// call to push can result in both a complete frame being delivered and a previous +/// frame being dropped. +/// +#[derive(Debug, Default)] +pub struct DepacketizerPushResult { + pub frame: Option, + pub drop_error: Option, +} + +impl From for DepacketizerPushResult { + fn from(frame: DepacketizerFrame) -> Self { + Self { frame: frame.into(), ..Default::default() } + } +} + +impl From for DepacketizerPushResult { + fn from(drop_event: DepacketizerDropError) -> Self { + Self { drop_error: drop_event.into(), ..Default::default() } + } +} + +/// An error indicating a frame was dropped. +#[derive(Debug, Error)] +#[error("Frame {frame_number} dropped: {reason}")] +pub struct DepacketizerDropError { + frame_number: u16, + reason: DepacketizerDropReason, +} + +/// Reason why a frame was dropped. +#[derive(Debug)] +pub enum DepacketizerDropReason { + /// Interrupted by the start of a new frame. + Interrupted, + /// Initial packet was never received. + UnknownFrame, + /// Reorder buffer is full. + BufferFull, + /// Not all packets received before final packet. + Incomplete { + /// Number of packets received. + received: u16, + /// Number of packets expected. + expected: u16, + }, +} + +impl Display for DepacketizerDropReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DepacketizerDropReason::Interrupted => write!(f, "interrupted"), + DepacketizerDropReason::UnknownFrame => write!(f, "unknown frame"), + DepacketizerDropReason::BufferFull => write!(f, "buffer full"), + DepacketizerDropReason::Incomplete { received, expected } => { + write!(f, "incomplete ({}/{})", received, expected) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::Counter; + use fake::{Fake, Faker}; + use test_case::test_case; + + #[test] + fn test_single_packet() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Single; + + let result = depacketizer.push(packet.clone()); + + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + + assert_eq!(frame.payload, packet.payload); + assert_eq!(frame.extensions, packet.header.extensions); + } + + #[test_case(0)] + #[test_case(8)] + #[test_case(Depacketizer::MAX_BUFFER_PACKETS - 2 ; "buffer_limit")] + fn test_multi_packet(inter_packets: usize) { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; + + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); + + for _ in 0..inter_packets { + packet.header.marker = FrameMarker::Inter; + packet.header.sequence = packet.header.sequence.wrapping_add(1); + + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); + } + + packet.header.marker = FrameMarker::Final; + packet.header.sequence = packet.header.sequence.wrapping_add(1); + + let result = depacketizer.push(packet.clone()); + + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + + assert_eq!(frame.extensions, packet.header.extensions); + assert_eq!(frame.payload.len(), packet.payload.len() * (inter_packets + 2)); + } + + #[test] + fn test_interrupted() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; + + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); + + let first_frame_number = packet.header.frame_number; + packet.header.frame_number += packet.header.frame_number.wrapping_add(1); // Next frame + + let result = depacketizer.push(packet); + assert!(result.frame.is_none()); + + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, first_frame_number); + assert!(matches!(drop.reason, DepacketizerDropReason::Interrupted)); + } + + #[test] + fn test_incomplete() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + let frame_number = packet.header.frame_number; + packet.header.marker = FrameMarker::Start; + + depacketizer.push(packet.clone()); + + packet.header.sequence += 3; + packet.header.marker = FrameMarker::Final; + + let result = depacketizer.push(packet); + assert!(result.frame.is_none()); + + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, frame_number); + assert!(matches!( + drop.reason, + DepacketizerDropReason::Incomplete { received: 2, expected: 4 } + )); + } + + #[test] + fn test_unknown_frame() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + let frame_number = packet.header.frame_number; + packet.header.marker = FrameMarker::Inter; + // Start packet for this frame will never be pushed. + + let result = depacketizer.push(packet); + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, frame_number); + assert!(matches!(drop.reason, DepacketizerDropReason::UnknownFrame)); + } + + #[test] + fn test_multi_frame() { + let mut depacketizer = Depacketizer::new(); + + let mut sequence = Counter::new(0); + for frame_number in 0..10 { + let mut packet: Packet = Faker.fake(); + packet.header.frame_number = frame_number; + packet.header.marker = FrameMarker::Start; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Inter; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Final; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet); + assert!(result.drop_error.is_none() && result.frame.is_some()); + } + } + + #[test] + fn test_duplicate_sequence_numbers() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; + packet.header.sequence = 1; + packet.payload = Bytes::from(vec![0xAB; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Inter; + packet.header.sequence = 1; // Same sequence number + packet.payload = Bytes::from(vec![0xCD; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Final; + packet.header.sequence = 2; + packet.payload = Bytes::from(vec![0xEF; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + + assert!(frame.payload.starts_with(&[0xCD; 3])); + // Should retain the second packet with duplicate sequence number + } + + impl fake::Dummy for Packet { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + let payload_len = rng.random_range(0..=1500); + let payload = (0..payload_len).map(|_| rng.random()).collect::(); + Self { header: Faker.fake_with_rng(rng), payload } + } + } +} diff --git a/livekit-datatrack/src/remote/events.rs b/livekit-datatrack/src/remote/events.rs new file mode 100644 index 000000000..ef32b4882 --- /dev/null +++ b/livekit-datatrack/src/remote/events.rs @@ -0,0 +1,149 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + api::{ + DataTrackFrame, DataTrackInfo, DataTrackSid, DataTrackSubscribeError, + DataTrackSubscribeOptions, RemoteDataTrack, + }, + packet::Handle, +}; +use bytes::Bytes; +use from_variants::FromVariants; +use std::collections::HashMap; +use tokio::sync::{broadcast, oneshot}; + +/// An external event handled by [`Manager`](super::manager::Manager). +#[derive(Debug, FromVariants)] +pub enum InputEvent { + SubscribeRequest(SubscribeRequest), + UnsubscribeRequest(UnsubscribeRequest), + SfuPublicationUpdates(SfuPublicationUpdates), + SfuSubscriberHandles(SfuSubscriberHandles), + /// Packet has been received over the transport. + PacketReceived(Bytes), + /// Resend all subscription updates. + /// + /// This must be sent after a full reconnect to ensure the SFU knows which + /// tracks are subscribed to locally. + /// + ResendSubscriptionUpdates, + /// Shutdown the manager, ending any subscriptions. + Shutdown, +} + +/// An event produced by [`Manager`](super::manager::Manager) requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + SfuUpdateSubscription(SfuUpdateSubscription), + TrackPublished(TrackPublished), + TrackUnpublished(TrackUnpublished), +} + +// MARK: - Input events + +/// Result of a [`SubscribeRequest`]. +pub(super) type SubscribeResult = + Result, DataTrackSubscribeError>; + +/// Client requested to subscribe to a data track. +/// +/// This is sent when the user calls [`RemoteDataTrack::subscribe`]. +/// +/// Only the first request to subscribe to a given track incurs meaningful overhead; subsequent +/// requests simply attach an additional receiver to the broadcast channel, allowing them to consume +/// frames from the existing subscription pipeline. +/// +#[derive(Debug)] +pub struct SubscribeRequest { + /// Identifier of the track. + pub(super) sid: DataTrackSid, + /// Options to use for the subscription. + pub(super) options: DataTrackSubscribeOptions, + /// Async completion channel. + pub(super) result_tx: oneshot::Sender, +} + +/// Client requested to unsubscribe from a data track. +#[derive(Debug)] +pub struct UnsubscribeRequest { + /// Identifier of the track to unsubscribe from. + pub(super) sid: DataTrackSid, +} + +/// SFU notification that track publications have changed. +/// +/// This event is produced from both [`livekit_protocol::JoinResponse`] and [`livekit_protocol::ParticipantUpdate`] +/// to provide a complete view of remote participants' track publications: +/// +/// - From a `JoinResponse`, it captures the initial set of tracks published when a participant joins. +/// - From a `ParticipantUpdate`, it captures subsequent changes (i.e., new tracks being +/// published and existing tracks unpublished). +/// +/// See [`event_from_join`](super::proto::event_from_join) and +/// [`event_from_participant_update`](super::proto::event_from_participant_update). +/// +#[derive(Debug)] +pub struct SfuPublicationUpdates { + /// Mapping between participant identity and data tracks currently + /// published by that participant. + pub updates: HashMap>, +} + +/// SFU notification that handles have been assigned for requested subscriptions. +/// +/// Protocol equivalent: [`livekit_protocol::DataTrackSubscriberHandles`]. +/// +#[derive(Debug)] +pub struct SfuSubscriberHandles { + /// Mapping between track handles attached to incoming packets to the + /// track SIDs they belong to. + pub mapping: HashMap, +} + +// MARK: - Output events + +/// Request sent to the SFU to update the subscription for a data track. +/// +/// Protocol equivalent: [`livekit_protocol::UpdateDataSubscription`]. +/// +#[derive(Debug)] +pub struct SfuUpdateSubscription { + /// Identifier of the affected track. + pub sid: DataTrackSid, + /// Whether to subscribe or unsubscribe. + pub subscribe: bool, +} + +/// A track has been published by a remote participant and is available to be +/// subscribed to. +/// +/// Emit a public event to deliver the track to the user, allowing them to subscribe +/// with [`RemoteDataTrack::subscribe`] if desired. +/// +#[derive(Debug)] +pub struct TrackPublished { + /// Track that was published. + pub track: RemoteDataTrack, +} + +/// A track has been unpublished by a remote participant. +/// +/// Emit a public event to inform the user. +/// +#[derive(Debug)] +pub struct TrackUnpublished { + /// SID of the track that was unpublished. + pub sid: DataTrackSid, +} diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs new file mode 100644 index 000000000..14447ceb5 --- /dev/null +++ b/livekit-datatrack/src/remote/manager.rs @@ -0,0 +1,1050 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + events::*, + pipeline::{Pipeline, PipelineOptions}, + RemoteDataTrack, RemoteTrackInner, +}; +use crate::{ + api::{DataTrackFrame, DataTrackInfo, DataTrackSid, DataTrackSubscribeError, InternalError}, + e2ee::DecryptionProvider, + packet::{Handle, Packet}, +}; +use anyhow::{anyhow, Context}; +use bytes::Bytes; +use std::{ + collections::{HashMap, HashSet}, + mem, + sync::Arc, +}; +use tokio::sync::{broadcast, mpsc, oneshot, watch}; +use tokio_stream::{wrappers::ReceiverStream, Stream}; + +/// Options for creating a [`Manager`]. +#[derive(Debug)] +pub struct ManagerOptions { + /// Provider to use for decrypting incoming frame payloads. + /// + /// If none, remote tracks using end-to-end encryption will not be available + /// for subscription. + /// + pub decryption_provider: Option>, +} + +/// System for managing data track subscriptions. +pub struct Manager { + decryption_provider: Option>, + event_in_tx: mpsc::Sender, + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, + + /// Mapping between track SID and descriptor. + descriptors: HashMap, + + /// Mapping between subscriber handle and track SID. + /// + /// This is an index that allows track descriptors to be looked up + /// by subscriber handle in O(1) time—necessary for routing incoming packets. + /// + sub_handles: HashMap, +} + +impl Manager { + /// Creates a new manager. + /// + /// Returns a tuple containing the following: + /// + /// - The manager itself to be spawned by the caller (see [`Manager::run`]). + /// - Channel for sending [`InputEvent`]s to be processed by the manager. + /// - Stream for receiving [`OutputEvent`]s produced by the manager. + /// + pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT); + let (event_out_tx, event_out_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT); + + let event_in = ManagerInput::new(event_in_tx.clone()); + let manager = Manager { + decryption_provider: options.decryption_provider, + event_in_tx, + event_in_rx, + event_out_tx, + descriptors: HashMap::default(), + sub_handles: HashMap::default(), + }; + + let event_out = ReceiverStream::new(event_out_rx); + (manager, event_in, event_out) + } + + /// Run the manager task, consuming self. + /// + /// The manager will continue running until receiving [`InputEvent::Shutdown`]. + /// + pub async fn run(mut self) { + log::debug!("Task started"); + while let Some(event) = self.event_in_rx.recv().await { + match event { + InputEvent::SubscribeRequest(event) => self.on_subscribe_request(event).await, + InputEvent::UnsubscribeRequest(event) => self.on_unsubscribe_request(event).await, + InputEvent::SfuPublicationUpdates(event) => { + self.on_sfu_publication_updates(event).await + } + InputEvent::SfuSubscriberHandles(event) => self.on_sfu_subscriber_handles(event), + InputEvent::PacketReceived(bytes) => self.on_packet_received(bytes), + InputEvent::ResendSubscriptionUpdates => { + self.on_resend_subscription_updates().await + } + InputEvent::Shutdown => break, + } + } + self.shutdown().await; + log::debug!("Task ended"); + } + + async fn on_subscribe_request(&mut self, event: SubscribeRequest) { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { + let error = DataTrackSubscribeError::Internal( + anyhow!("Cannot subscribe to unknown track").into(), + ); + _ = event.result_tx.send(Err(error)); + return; + }; + match &mut descriptor.subscription { + SubscriptionState::None => { + let update_event = SfuUpdateSubscription { sid: event.sid, subscribe: true }; + _ = self.event_out_tx.send(update_event.into()).await; + descriptor.subscription = SubscriptionState::Pending { + result_txs: vec![event.result_tx], + buffer_size: event.options.buffer_size, + }; + // TODO: schedule timeout internally + } + SubscriptionState::Pending { result_txs, .. } => { + result_txs.push(event.result_tx); + } + SubscriptionState::Active { frame_tx, .. } => { + let frame_rx = frame_tx.subscribe(); + _ = event.result_tx.send(Ok(frame_rx)) + } + } + } + + async fn on_unsubscribe_request(&mut self, event: UnsubscribeRequest) { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { + return; + }; + + let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription else { + log::warn!("Unexpected state"); + return; + }; + descriptor.subscription = SubscriptionState::None; + self.sub_handles.remove(&sub_handle); + + let event = SfuUpdateSubscription { sid: event.sid, subscribe: false }; + _ = self.event_out_tx.send(event.into()).await; + } + + async fn on_sfu_publication_updates(&mut self, event: SfuPublicationUpdates) { + if event.updates.is_empty() { + return; + } + let mut participant_to_sids: HashMap> = HashMap::new(); + + // Detect published tracks + for (publisher_identity, tracks) in event.updates { + let sids_in_update = participant_to_sids.entry(publisher_identity.clone()).or_default(); + for info in tracks { + let sid = info.sid(); + sids_in_update.insert(sid.clone()); + if self.descriptors.contains_key(&sid) { + continue; + } + self.handle_track_published(publisher_identity.clone(), info).await; + } + } + + // Detect unpublished tracks (scoped per publisher in the update) + for (publisher_identity, sids_in_update) in &participant_to_sids { + let unpublished_sids: Vec<_> = self + .descriptors + .iter() + .filter(|(_, desc)| desc.publisher_identity.as_ref() == publisher_identity) + .filter(|(sid, _)| !sids_in_update.contains(*sid)) + .map(|(sid, _)| sid.clone()) + .collect(); + for sid in unpublished_sids { + self.handle_track_unpublished(sid).await; + } + } + } + + async fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { + let sid = info.sid(); + if self.descriptors.contains_key(&sid) { + log::error!("Existing descriptor for track {}", sid); + return; + } + let info = Arc::new(info); + let publisher_identity: Arc = publisher_identity.into(); + + let (published_tx, published_rx) = watch::channel(true); + + let descriptor = Descriptor { + info: info.clone(), + publisher_identity: publisher_identity.clone(), + published_tx, + subscription: SubscriptionState::None, + }; + self.descriptors.insert(sid, descriptor); + + let inner = RemoteTrackInner { + published_rx, + event_in_tx: self.event_in_tx.downgrade(), // TODO: wrap + publisher_identity, + }; + let track = RemoteDataTrack::new(info, inner); + _ = self.event_out_tx.send(TrackPublished { track }.into()).await; + } + + async fn handle_track_unpublished(&mut self, sid: DataTrackSid) { + let Some(descriptor) = self.descriptors.remove(&sid) else { + log::error!("Unknown track {}", sid); + return; + }; + if let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription { + self.sub_handles.remove(&sub_handle); + }; + _ = descriptor.published_tx.send(false); + _ = self.event_out_tx.send(TrackUnpublished { sid }.into()).await; + } + + fn on_sfu_subscriber_handles(&mut self, event: SfuSubscriberHandles) { + for (handle, sid) in event.mapping { + self.register_subscriber_handle(handle, sid); + } + } + + fn register_subscriber_handle(&mut self, assigned_handle: Handle, sid: DataTrackSid) { + let Some(descriptor) = self.descriptors.get_mut(&sid) else { + log::warn!("Unknown track: {}", sid); + return; + }; + let (result_txs, buffer_size) = match &mut descriptor.subscription { + SubscriptionState::None => { + // Handle assigned when there is no pending or active subscription is unexpected. + log::warn!("No subscription for {}", sid); + return; + } + SubscriptionState::Active { sub_handle, .. } => { + // Update handle for an active subscription. This can occur following a full reconnect. + self.sub_handles.remove(sub_handle); + *sub_handle = assigned_handle; + self.sub_handles.insert(assigned_handle, sid); + return; + } + SubscriptionState::Pending { result_txs, buffer_size } => { + // Handle assigned for pending subscription, transition to active. + (mem::take(result_txs), *buffer_size) + } + }; + + let (packet_tx, packet_rx) = mpsc::channel(Self::PACKET_BUFFER_COUNT); + let (frame_tx, frame_rx) = broadcast::channel(buffer_size); + + let decryption_provider = if descriptor.info.uses_e2ee() { + self.decryption_provider.as_ref().map(Arc::clone) + } else { + None + }; + + let pipeline_opts = PipelineOptions { + info: descriptor.info.clone(), + publisher_identity: descriptor.publisher_identity.clone(), + decryption_provider, + }; + let pipeline = Pipeline::new(pipeline_opts); + + let track_task = TrackTask { + info: descriptor.info.clone(), + pipeline, + published_rx: descriptor.published_tx.subscribe(), + packet_rx, + frame_tx: frame_tx.clone(), + event_in_tx: self.event_in_tx.clone(), + }; + let task_handle = livekit_runtime::spawn(track_task.run()); + + descriptor.subscription = SubscriptionState::Active { + sub_handle: assigned_handle, + packet_tx, + frame_tx, + task_handle, + }; + self.sub_handles.insert(assigned_handle, sid); + + for result_tx in result_txs { + _ = result_tx.send(Ok(frame_rx.resubscribe())); + } + } + + fn on_packet_received(&mut self, bytes: Bytes) { + let packet = match Packet::deserialize(bytes) { + Ok(packet) => packet, + Err(err) => { + log::error!("Failed to deserialize packet: {}", err); + return; + } + }; + let Some(sid) = self.sub_handles.get(&packet.header.track_handle) else { + log::warn!("Unknown subscriber handle {}", packet.header.track_handle); + return; + }; + let Some(descriptor) = self.descriptors.get(sid) else { + log::warn!("Missing descriptor for track {}", sid); + return; + }; + let SubscriptionState::Active { packet_tx, .. } = &descriptor.subscription else { + log::warn!("Received packet for track {} without subscription", sid); + return; + }; + _ = packet_tx + .try_send(packet) + .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); + } + + async fn on_resend_subscription_updates(&self) { + let update_events = + self.descriptors.iter().filter_map(|(sid, descriptor)| match descriptor.subscription { + SubscriptionState::None => None, + SubscriptionState::Pending { .. } | SubscriptionState::Active { .. } => { + Some(SfuUpdateSubscription { sid: sid.clone(), subscribe: true }) + } + }); + for event in update_events { + _ = self.event_out_tx.send(event.into()).await; + } + } + + /// Performs cleanup before the task ends. + async fn shutdown(self) { + for (_, descriptor) in self.descriptors { + _ = descriptor.published_tx.send(false); + match descriptor.subscription { + SubscriptionState::None => {} + SubscriptionState::Pending { result_txs, .. } => { + for result_tx in result_txs { + _ = result_tx.send(Err(DataTrackSubscribeError::Disconnected)); + } + } + SubscriptionState::Active { task_handle, .. } => task_handle.await, + } + } + } + + /// Maximum number of incoming packets to buffer per track to be sent + /// to the track's pipeline. + const PACKET_BUFFER_COUNT: usize = 16; + + /// Maximum number of input and output events to buffer. + const EVENT_BUFFER_COUNT: usize = 16; +} + +/// Information and state for a remote data track. +#[derive(Debug)] +struct Descriptor { + info: Arc, + publisher_identity: Arc, + published_tx: watch::Sender, + subscription: SubscriptionState, +} + +#[derive(Debug)] +enum SubscriptionState { + /// Track is not subscribed to. + None, + /// Track is being subscribed to, waiting for subscriber handle. + Pending { + /// All currently pending requests to subscribe to the track. + result_txs: Vec>, + /// Internal frame buffer size to use once active. + buffer_size: usize, + }, + /// Track has an active subscription. + Active { + sub_handle: Handle, + packet_tx: mpsc::Sender, + frame_tx: broadcast::Sender, + task_handle: livekit_runtime::JoinHandle<()>, + }, +} + +/// Task for an individual data track with an active subscription. +struct TrackTask { + info: Arc, + pipeline: Pipeline, + published_rx: watch::Receiver, + packet_rx: mpsc::Receiver, + frame_tx: broadcast::Sender, + event_in_tx: mpsc::Sender, +} + +impl TrackTask { + async fn run(mut self) { + log::debug!("Track task started: name={}", self.info.name); + + let mut is_published = *self.published_rx.borrow(); + while is_published { + tokio::select! { + biased; // State updates take priority + _ = self.published_rx.changed() => { + is_published = *self.published_rx.borrow(); + }, + _ = self.frame_tx.closed() => { + let event = UnsubscribeRequest { sid: self.info.sid() }; + _ = self.event_in_tx.send(event.into()).await; + break; // No more subscribers + }, + Some(packet) = self.packet_rx.recv() => { + self.receive(packet); + }, + else => break + } + } + + log::debug!("Track task ended: name={}", self.info.name); + } + + fn receive(&mut self, packet: Packet) { + let Some(frame) = self.pipeline.process_packet(packet) else { return }; + _ = self + .frame_tx + .send(frame) + .inspect_err(|err| log::debug!("Cannot send frame to subscribers: {}", err)); + } +} + +/// Channel for sending [`InputEvent`]s to [`Manager`]. +#[derive(Debug, Clone)] +pub struct ManagerInput { + event_in_tx: mpsc::Sender, + _drop_guard: Arc, +} + +/// Guard that sends shutdown event when the last reference is dropped. +#[derive(Debug)] +struct DropGuard { + event_in_tx: mpsc::Sender, +} + +impl Drop for DropGuard { + fn drop(&mut self) { + _ = self.event_in_tx.try_send(InputEvent::Shutdown); + } +} + +impl ManagerInput { + fn new(event_in_tx: mpsc::Sender) -> Self { + Self { event_in_tx: event_in_tx.clone(), _drop_guard: DropGuard { event_in_tx }.into() } + } + + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + api::DataTrackSubscribeOptions, + e2ee::{DecryptionError, DecryptionProvider, EncryptedPayload}, + packet::{E2eeExt, Extensions, FrameMarker, Header, Timestamp}, + utils::testing::expect_event, + }; + use fake::{Fake, Faker}; + use futures_util::{future::join, StreamExt}; + use std::{collections::HashMap, sync::RwLock, time::Duration}; + use test_case::test_case; + use tokio::time; + + #[derive(Debug)] + struct PrefixStrippingDecryptor; + + impl DecryptionProvider for PrefixStrippingDecryptor { + fn decrypt( + &self, + payload: EncryptedPayload, + _sender_identity: &str, + ) -> Result { + Ok(payload.payload.slice(4..)) + } + } + + #[tokio::test] + async fn test_manager_task_shutdown() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, _) = Manager::new(options); + + let join_handle = livekit_runtime::spawn(manager.run()); + _ = input.send(InputEvent::Shutdown); + + time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); + } + + #[test_case(true; "via_unpublish")] + #[test_case(false; "via_unsubscribe")] + #[tokio::test] + async fn test_track_task_shutdown(via_unpublish: bool) { + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let info = Arc::new(info); + let sid = info.sid(); + let publisher_identity: Arc = Faker.fake::().into(); + + let pipeline_opts = + PipelineOptions { info: info.clone(), publisher_identity, decryption_provider: None }; + let pipeline = Pipeline::new(pipeline_opts); + + let (published_tx, published_rx) = watch::channel(true); + let (_packet_tx, packet_rx) = mpsc::channel(4); + let (frame_tx, frame_rx) = broadcast::channel(4); + let (event_in_tx, mut event_in_rx) = mpsc::channel(4); + + let task = + TrackTask { info: info, pipeline, published_rx, packet_rx, frame_tx, event_in_tx }; + let task_handle = livekit_runtime::spawn(task.run()); + + let trigger_shutdown = async { + if via_unpublish { + // Simulates SFU publication update + published_tx.send(false).unwrap(); + return; + } + // Simulates all subscribers dropped + mem::drop(frame_rx); + + while let Some(event) = event_in_rx.recv().await { + let InputEvent::UnsubscribeRequest(event) = event else { + panic!("Unexpected event type"); + }; + assert_eq!(event.sid, sid); + return; + } + panic!("Did not receive unsubscribe"); + }; + time::timeout(Duration::from_secs(1), join(task_handle, trigger_shutdown)).await.unwrap(); + } + + #[tokio::test] + async fn test_subscribe() { + let publisher_identity: String = Faker.fake(); + let track_name: String = Faker.fake(); + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + // Simulate track published + let event = SfuPublicationUpdates { + updates: HashMap::from([( + publisher_identity.clone(), + vec![DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), // Pub handle + name: track_name.clone(), + uses_e2ee: false, + }], + )]), + }; + _ = input.send(event.into()); + + let wait_for_track = async { + while let Some(event) = output.next().await { + match event { + OutputEvent::TrackPublished(track) => return track, + _ => continue, + } + } + panic!("No track received"); + }; + + let track = wait_for_track.await.track; + assert!(track.is_published()); + assert_eq!(track.info().name, track_name); + assert_eq!(track.info().sid(), track_sid); + assert_eq!(track.publisher_identity(), publisher_identity); + + let simulate_subscriber_handles = async { + while let Some(event) = output.next().await { + match event { + OutputEvent::SfuUpdateSubscription(event) => { + assert!(event.subscribe); + assert_eq!(event.sid, track_sid); + time::sleep(Duration::from_millis(20)).await; + + // Simulate SFU reply + let event = SfuSubscriberHandles { + mapping: HashMap::from([(sub_handle, track_sid.clone())]), + }; + _ = input.send(event.into()); + } + _ => {} + } + } + }; + + time::timeout(Duration::from_secs(1), async { + tokio::select! { + _ = simulate_subscriber_handles => {} + _ = track.subscribe() => {} + } + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_track_publication_add_and_remove() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = + SfuPublicationUpdates { updates: HashMap::from([("identity1".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + + let track = expect_event!(output, OutputEvent::TrackPublished).track; + assert_eq!(track.info().sid(), track_sid); + assert_eq!(track.info().name, "test"); + assert!(track.is_published()); + + // Simulate track unpublished + let event = + SfuPublicationUpdates { updates: HashMap::from([("identity1".into(), vec![])]) }; + input.send(event.into()).unwrap(); + + time::timeout(Duration::from_secs(1), track.wait_for_unpublish()).await.unwrap(); + assert!(!track.is_published()); + + let event = expect_event!(output, OutputEvent::TrackUnpublished); + assert_eq!(event.sid, track_sid); + } + + #[tokio::test] + async fn test_sfu_publication_updates_idempotent() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate three identical publication updates + for _ in 0..3 { + let event = SfuPublicationUpdates { + updates: HashMap::from([("identity1".into(), vec![info.clone()])]), + }; + input.send(event.into()).unwrap(); + } + + expect_event!(output, OutputEvent::TrackPublished); + + // Drain remaining events; no second TrackAvailable should appear + input.send(InputEvent::Shutdown).unwrap(); + while let Some(event) = output.next().await { + assert!(!matches!(event, OutputEvent::TrackPublished(_))); + } + } + + #[tokio::test] + async fn test_subscribe_receives_frame() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // Subscribe to the track + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + assert_eq!(event.sid, track_sid); + + // Simulate SFU assigning subscriber handle + let event = SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid)]) }; + input.send(event.into()).unwrap(); + + let mut frame_rx = + time::timeout(Duration::from_secs(1), result_rx).await.unwrap().unwrap().unwrap(); + + // Simulate receiving a single-frame packet + let packet = Packet { + header: Header { + marker: FrameMarker::Single, + track_handle: sub_handle, + sequence: 0, + frame_number: 0, + timestamp: Timestamp::from_ticks(0), + extensions: Extensions::default(), + }, + payload: Bytes::from_static(&[1, 2, 3, 4, 5]), + }; + input.send(InputEvent::PacketReceived(packet.serialize())).unwrap(); + + let frame = time::timeout(Duration::from_secs(1), frame_rx.recv()).await.unwrap().unwrap(); + assert_eq!(frame.payload.as_ref(), &[1, 2, 3, 4, 5]); + } + + #[tokio::test] + async fn test_subscribe_with_e2ee() { + let options = + ManagerOptions { decryption_provider: Some(Arc::new(PrefixStrippingDecryptor)) }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: true, + }; + + // Simulate track published (with e2ee) + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // Subscribe to the track + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + + // Simulate SFU assigning subscriber handle + let event = SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid)]) }; + input.send(event.into()).unwrap(); + + let mut frame_rx = + time::timeout(Duration::from_secs(1), result_rx).await.unwrap().unwrap().unwrap(); + + // Simulate receiving an encrypted single-frame packet + let packet = Packet { + header: Header { + marker: FrameMarker::Single, + track_handle: sub_handle, + sequence: 0, + frame_number: 0, + timestamp: Timestamp::from_ticks(0), + extensions: Extensions { + e2ee: Some(E2eeExt { key_index: 0, iv: [0; 12] }), + ..Default::default() + }, + }, + payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF, 1, 2, 3, 4, 5]), + }; + input.send(InputEvent::PacketReceived(packet.serialize())).unwrap(); + + // Payload should have fake encryption prefix stripped by decryptor + let frame = time::timeout(Duration::from_secs(1), frame_rx.recv()).await.unwrap().unwrap(); + assert_eq!(frame.payload.as_ref(), &[1, 2, 3, 4, 5]); + } + + #[tokio::test] + async fn test_subscribe_fan_out_to_multiple_subscribers() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // First subscriber triggers SFU interaction + let (result_tx1, result_rx1) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx: result_tx1, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + + // Simulate SFU assigning subscriber handle + let event = + SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid.clone())]) }; + input.send(event.into()).unwrap(); + + let mut rx1 = + time::timeout(Duration::from_secs(1), result_rx1).await.unwrap().unwrap().unwrap(); + + // Additional subscribers attach directly (no further SFU interaction) + let (result_tx2, result_rx2) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx: result_tx2, + }; + input.send(event.into()).unwrap(); + let mut rx2 = result_rx2.await.unwrap().unwrap(); + + let (result_tx3, result_rx3) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx: result_tx3, + }; + input.send(event.into()).unwrap(); + let mut rx3 = result_rx3.await.unwrap().unwrap(); + + // Simulate receiving a single-frame packet + let packet = Packet { + header: Header { + marker: FrameMarker::Single, + track_handle: sub_handle, + sequence: 0, + frame_number: 0, + timestamp: Timestamp::from_ticks(0), + extensions: Extensions::default(), + }, + payload: Bytes::from_static(&[1, 2, 3, 4, 5]), + }; + input.send(InputEvent::PacketReceived(packet.serialize())).unwrap(); + + // All subscribers should receive the same frame + for rx in [&mut rx1, &mut rx2, &mut rx3] { + let frame = time::timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap(); + assert_eq!(frame.payload.as_ref(), &[1, 2, 3, 4, 5]); + } + } + + #[tokio::test] + async fn test_subscribe_unknown_track_fails() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, _) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + // Subscribe to a track that was never published + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: Faker.fake(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let result = result_rx.await.unwrap(); + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_unpublish_terminates_pending_subscription() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // Subscribe (enters Pending state) + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + + // Simulate track unpublished before SFU assigns a handle + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![])]) }; + input.send(event.into()).unwrap(); + + let result = time::timeout(Duration::from_secs(1), result_rx).await.unwrap(); + assert!(result.is_err()); + + let event = expect_event!(output, OutputEvent::TrackUnpublished); + assert_eq!(event.sid, track_sid); + } + + #[tokio::test] + async fn test_unpublish_terminates_active_subscription() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // Subscribe to the track + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + + // Simulate SFU assigning subscriber handle + let event = + SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid.clone())]) }; + input.send(event.into()).unwrap(); + + let mut frame_rx = + time::timeout(Duration::from_secs(1), result_rx).await.unwrap().unwrap().unwrap(); + + // Simulate track unpublished while subscription is active + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![])]) }; + input.send(event.into()).unwrap(); + + let result = time::timeout(Duration::from_secs(1), frame_rx.recv()).await.unwrap(); + assert!(result.is_err()); + + let event = expect_event!(output, OutputEvent::TrackUnpublished); + assert_eq!(event.sid, track_sid); + } + + #[tokio::test] + async fn test_all_subscribers_dropped_terminates_sfu_subscription() { + let options = ManagerOptions { decryption_provider: None }; + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); + + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); + let info = DataTrackInfo { + sid: RwLock::new(track_sid.clone()).into(), + pub_handle: Faker.fake(), + name: "test".into(), + uses_e2ee: false, + }; + + // Simulate track published + let event = SfuPublicationUpdates { updates: HashMap::from([("id".into(), vec![info])]) }; + input.send(event.into()).unwrap(); + expect_event!(output, OutputEvent::TrackPublished); + + // Subscribe to the track + let (result_tx, result_rx) = oneshot::channel(); + let event = SubscribeRequest { + sid: track_sid.clone(), + options: DataTrackSubscribeOptions::default(), + result_tx, + }; + input.send(event.into()).unwrap(); + + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(event.subscribe); + + // Simulate SFU assigning subscriber handle + let event = + SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid.clone())]) }; + input.send(event.into()).unwrap(); + + let frame_rx = + time::timeout(Duration::from_secs(1), result_rx).await.unwrap().unwrap().unwrap(); + + // Drop the only subscriber + drop(frame_rx); + + // Manager should request SFU to unsubscribe + let event = expect_event!(output, OutputEvent::SfuUpdateSubscription); + assert!(!event.subscribe); + assert_eq!(event.sid, track_sid); + } +} diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs new file mode 100644 index 000000000..d3ff69738 --- /dev/null +++ b/livekit-datatrack/src/remote/mod.rs @@ -0,0 +1,226 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; +use events::{InputEvent, SubscribeRequest}; +use livekit_runtime::timeout; +use std::{ + marker::PhantomData, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; +use thiserror::Error; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio_stream::{wrappers::BroadcastStream, Stream}; + +pub(crate) mod events; +pub(crate) mod manager; +pub(crate) mod proto; + +mod depacketizer; +mod pipeline; + +/// Data track published by a remote participant. +pub type RemoteDataTrack = DataTrack; + +/// Marker type indicating a [`DataTrack`] belongs to a remote participant. +/// +/// See also: [`RemoteDataTrack`] +/// +#[derive(Debug, Clone)] +pub struct Remote; + +impl DataTrack { + pub(crate) fn new(info: Arc, inner: RemoteTrackInner) -> Self { + Self { info, inner: Arc::new(inner.into()), _location: PhantomData } + } + + fn inner(&self) -> &RemoteTrackInner { + match &*self.inner { + DataTrackInner::Remote(inner) => inner, + DataTrackInner::Local(_) => unreachable!(), // Safe (type state) + } + } +} + +impl DataTrack { + /// Subscribes to the data track. + /// + /// # Returns + /// + /// A stream that yields [`DataTrackFrame`]s as they arrive. + /// + /// # Options + /// + /// To set custom subscription options, see [`Self::subscribe_with_options`]. + /// + /// # Multiple Subscriptions + /// + /// An application may call `subscribe` more than once to process frames in + /// multiple places. For example, one async task might plot values on a graph + /// while another writes them to a file. + /// + /// Internally, only the first call to `subscribe` communicates with the SFU and + /// allocates the resources required to receive frames. Additional subscriptions + /// reuse the same underlying pipeline and do not trigger additional signaling. + /// + /// Note that newly created subscriptions only receive frames published after + /// the initial subscription is established. + /// + pub async fn subscribe(&self) -> Result { + self.subscribe_with_options(DataTrackSubscribeOptions::default()).await + } + + /// Subscribes to the data track, specifying custom options. + /// + /// Same usage and return as [`Self::subscribe`] with an additional argument + /// to specify options. + /// + pub async fn subscribe_with_options( + &self, + options: DataTrackSubscribeOptions, + ) -> Result { + let (result_tx, result_rx) = oneshot::channel(); + let subscribe_event = SubscribeRequest { sid: self.info.sid(), options, result_tx }; + self.inner() + .event_in_tx + .upgrade() + .ok_or(DataTrackSubscribeError::Disconnected)? + .send(subscribe_event.into()) + .await + .map_err(|_| DataTrackSubscribeError::Disconnected)?; + + // TODO: standardize timeout + let frame_rx = timeout(Duration::from_secs(10), result_rx) + .await + .map_err(|_| DataTrackSubscribeError::Timeout)? + .map_err(|_| DataTrackSubscribeError::Disconnected)??; + + Ok(DataTrackSubscription { inner: BroadcastStream::new(frame_rx) }) + } + + /// Identity of the participant who published the track. + pub fn publisher_identity(&self) -> &str { + &self.inner().publisher_identity + } +} + +/// A stream of [`DataTrackFrame`]s received from a [`RemoteDataTrack`]. +pub struct DataTrackSubscription { + inner: BroadcastStream, +} + +impl Stream for DataTrackSubscription { + type Item = DataTrackFrame; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + loop { + match Pin::new(&mut this.inner).poll_next(cx) { + Poll::Ready(Some(Ok(frame))) => return Poll::Ready(Some(frame)), + Poll::Ready(Some(Err(_))) => continue, + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => return Poll::Pending, + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct RemoteTrackInner { + publisher_identity: Arc, + published_rx: watch::Receiver, + event_in_tx: mpsc::WeakSender, +} + +impl RemoteTrackInner { + pub(crate) fn is_published(&self) -> bool { + *self.published_rx.borrow() + } + + pub(crate) async fn wait_for_unpublish(&self) { + let mut published_rx = self.published_rx.clone(); + _ = published_rx.wait_for(|is_published| !*is_published).await + } +} + +#[derive(Debug, Error)] +pub enum DataTrackSubscribeError { + #[error("The track has been unpublished and is no longer available")] + Unpublished, + #[error("Request to subscribe to data track timed-out")] + Timeout, + #[error("Cannot subscribe to data track when disconnected")] + Disconnected, + #[error(transparent)] + Internal(#[from] InternalError), +} + +/// Options for subscribing to a data track. +/// +/// # Examples +/// +/// Specify a custom buffer size: +/// +/// ``` +/// # use livekit_datatrack::api::DataTrackSubscribeOptions; +/// let options = DataTrackSubscribeOptions::default() +/// .with_buffer_size(128); // Buffer 128 frames internally +/// +/// # assert_eq!(options.buffer_size(), 128); +/// ``` +/// +#[derive(Debug, Clone)] +pub struct DataTrackSubscribeOptions { + buffer_size: usize, +} + +impl DataTrackSubscribeOptions { + /// Creates subscribe options with default values. + /// + /// Equivalent to [`Self::default`]. + /// + pub fn new() -> Self { + Self { buffer_size: 16 } + } + + /// Returns the maximum number of received frames buffered internally. + pub fn buffer_size(&self) -> usize { + self.buffer_size + } + + /// Sets the maximum number of received frames buffered internally. + /// + /// Zero is not a valid buffer size; if a value of zero is provided, it will be clamped to one. + /// + /// Note: if there is already an active subscription for a given track, specifying a + /// different buffer size when obtaining a new subscription will have no effect. + /// + pub fn with_buffer_size(mut self, mut frames: usize) -> Self { + if frames == 0 { + log::warn!("Zero is not a valid buffer size, using one"); + frames = 1; + } + self.buffer_size = frames; + self + } +} + +impl Default for DataTrackSubscribeOptions { + fn default() -> Self { + Self::new() + } +} diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs new file mode 100644 index 000000000..5cc8c2ff3 --- /dev/null +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -0,0 +1,124 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::depacketizer::{Depacketizer, DepacketizerFrame}; +use crate::{ + api::{DataTrackFrame, DataTrackInfo}, + e2ee::{DecryptionProvider, EncryptedPayload}, + packet::Packet, +}; +use std::sync::Arc; + +/// Options for creating a [`Pipeline`]. +pub(super) struct PipelineOptions { + pub info: Arc, + pub publisher_identity: Arc, + pub decryption_provider: Option>, +} + +/// Pipeline for an individual data track subscription. +pub(super) struct Pipeline { + publisher_identity: Arc, + e2ee_provider: Option>, + depacketizer: Depacketizer, +} + +impl Pipeline { + /// Creates a new pipeline with the given options. + pub fn new(options: PipelineOptions) -> Self { + debug_assert_eq!(options.info.uses_e2ee, options.decryption_provider.is_some()); + let depacketizer = Depacketizer::new(); + Self { + publisher_identity: options.publisher_identity, + e2ee_provider: options.decryption_provider, + depacketizer, + } + } + + pub fn process_packet(&mut self, packet: Packet) -> Option { + let frame = self.depacketize(packet)?; + let frame = self.decrypt_if_needed(frame)?; + Some(frame.into()) + } + + /// Depacketize the given frame, log if a drop occurs. + fn depacketize(&mut self, packet: Packet) -> Option { + let result = self.depacketizer.push(packet); + if let Some(drop) = result.drop_error { + // In a future version, use this to maintain drop statistics. + log::debug!("{}", drop); + }; + result.frame + } + + /// Decrypt the frame's payload if E2EE is enabled for this track. + fn decrypt_if_needed(&self, mut frame: DepacketizerFrame) -> Option { + let Some(decryption) = &self.e2ee_provider else { return frame.into() }; + + let Some(e2ee) = frame.extensions.e2ee else { + log::error!("Missing E2EE meta"); + return None; + }; + + let encrypted = + EncryptedPayload { payload: frame.payload, iv: e2ee.iv, key_index: e2ee.key_index }; + frame.payload = match decryption.decrypt(encrypted, &self.publisher_identity) { + Ok(decrypted) => decrypted, + Err(err) => { + log::error!("{}", err); + return None; + } + }; + frame.into() + } +} + +impl From for DataTrackFrame { + fn from(frame: DepacketizerFrame) -> Self { + Self { + payload: frame.payload, + user_timestamp: frame.extensions.user_timestamp.map(|v| v.0), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::{FrameMarker, Header}; + use fake::{Fake, Faker}; + + #[test] + fn test_process_frame() { + const PAYLOAD_LEN: usize = 1024; + + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let publisher_identity: Arc = Faker.fake::().into(); + + let options = + PipelineOptions { info: info.into(), publisher_identity, decryption_provider: None }; + let mut pipeline = Pipeline::new(options); + + let mut header: Header = Faker.fake(); + header.marker = FrameMarker::Single; + header.extensions.e2ee = None; + + let frame = Packet { header, payload: vec![Faker.fake(); PAYLOAD_LEN].into() }; + + let frame = pipeline.process_packet(frame).expect("Should return a frame"); + assert_eq!(frame.payload.len(), PAYLOAD_LEN); + } +} diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs new file mode 100644 index 000000000..187c01ca9 --- /dev/null +++ b/livekit-datatrack/src/remote/proto.rs @@ -0,0 +1,159 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::events::*; +use crate::{ + api::{DataTrackInfo, DataTrackSid, InternalError}, + packet::Handle, +}; +use livekit_protocol as proto; +use std::{collections::HashMap, mem}; + +// MARK: - Protocol -> input event + +impl TryFrom for SfuSubscriberHandles { + type Error = InternalError; + + fn try_from(msg: proto::DataTrackSubscriberHandles) -> Result { + let mapping = msg + .sub_handles + .into_iter() + .map(|(handle, info)| -> Result<_, InternalError> { + let handle: Handle = handle.try_into().map_err(anyhow::Error::from)?; + let sid: DataTrackSid = info.track_sid.try_into().map_err(anyhow::Error::from)?; + Ok((handle, sid)) + }) + .collect::, _>>()?; + Ok(SfuSubscriberHandles { mapping }) + } +} + +/// Extracts an [`SfuPublicationUpdates`] event from a join response. +/// +/// This takes ownership of the `data_tracks` vector for each participant +/// (except for the local participant), leaving an empty vector in its place. +/// +pub fn event_from_join( + msg: &mut proto::JoinResponse, +) -> Result { + event_from_participant_info(&mut msg.other_participants, None) +} + +/// Extracts an [`SfuPublicationUpdates`] event from a participant update. +/// +/// This takes ownership of the `data_tracks` vector for each participant in +/// the update, leaving an empty vector in its place. +/// +pub fn event_from_participant_update( + msg: &mut proto::ParticipantUpdate, + local_participant_identity: &str, +) -> Result { + // TODO: is there a better way to exclude the local participant? + event_from_participant_info(&mut msg.participants, local_participant_identity.into()) +} + +fn event_from_participant_info( + msg: &mut [proto::ParticipantInfo], + local_participant_identity: Option<&str>, +) -> Result { + let updates = msg + .iter_mut() + .filter(|participant| { + local_participant_identity.is_none_or(|identity| participant.identity != identity) + }) + .map(|participant| -> Result<_, InternalError> { + Ok((participant.identity.clone(), extract_track_info(participant)?)) + }) + .collect::>, _>>()?; + Ok(SfuPublicationUpdates { updates }) +} + +fn extract_track_info( + msg: &mut proto::ParticipantInfo, +) -> Result, InternalError> { + mem::take(&mut msg.data_tracks) + .into_iter() + .map(TryInto::::try_into) + .collect::, InternalError>>() +} + +// MARK: - Output event -> protocol + +impl From for proto::UpdateDataSubscription { + fn from(event: SfuUpdateSubscription) -> Self { + let update = proto::update_data_subscription::Update { + track_sid: event.sid.into(), + subscribe: event.subscribe, + options: Default::default(), + }; + Self { updates: vec![update] } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_subscriber_handles() { + let sub_handles = [ + ( + 1, + proto::data_track_subscriber_handles::PublishedDataTrack { + track_sid: "DTR_1234".into(), + ..Default::default() + }, + ), + ( + 2, + proto::data_track_subscriber_handles::PublishedDataTrack { + track_sid: "DTR_4567".into(), + ..Default::default() + }, + ), + ]; + let subscriber_handles = + proto::DataTrackSubscriberHandles { sub_handles: HashMap::from(sub_handles) }; + + let event: SfuSubscriberHandles = subscriber_handles.try_into().unwrap(); + assert_eq!( + event.mapping.get(&1u32.try_into().unwrap()).unwrap(), + &"DTR_1234".to_string().try_into().unwrap() + ); + assert_eq!( + event.mapping.get(&2u32.try_into().unwrap()).unwrap(), + &"DTR_4567".to_string().try_into().unwrap() + ); + } + + #[test] + fn test_extract_track_info() { + let data_tracks = vec![proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track1".into(), + encryption: proto::encryption::Type::Gcm.into(), + }]; + let mut participant_info = proto::ParticipantInfo { data_tracks, ..Default::default() }; + + let track_info = extract_track_info(&mut participant_info).unwrap(); + assert!(participant_info.data_tracks.is_empty(), "Expected original vec taken"); + assert_eq!(track_info.len(), 1); + + let first = track_info.first().unwrap(); + assert_eq!(first.pub_handle, 1u32.try_into().unwrap()); + assert_eq!(first.name, "track1"); + assert_eq!(*first.sid.read().unwrap(), "DTR_1234".to_string().try_into().unwrap()); + } +} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs new file mode 100644 index 000000000..8c313b2b1 --- /dev/null +++ b/livekit-datatrack/src/track.rs @@ -0,0 +1,146 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::packet::Handle; +use from_variants::FromVariants; +use std::{ + fmt::Display, + marker::PhantomData, + sync::{Arc, RwLock}, +}; +use thiserror::Error; + +/// Track for communicating application-specific data between participants in room. +#[derive(Debug, Clone)] +pub struct DataTrack { + pub(crate) info: Arc, + pub(crate) inner: Arc, + /// Marker indicating local or remote. + pub(crate) _location: PhantomData, +} + +#[derive(Debug, Clone, FromVariants)] +pub(crate) enum DataTrackInner { + Local(crate::local::LocalTrackInner), + Remote(crate::remote::RemoteTrackInner), +} + +impl DataTrack { + /// Information about the data track. + pub fn info(&self) -> &DataTrackInfo { + &self.info + } + + /// Whether or not the track is still published. + pub fn is_published(&self) -> bool { + match self.inner.as_ref() { + DataTrackInner::Local(inner) => inner.is_published(), + DataTrackInner::Remote(inner) => inner.is_published(), + } + } + + /// Waits asynchronously until the track is unpublished. + /// + /// Use this to trigger follow-up work once the track is no longer published. + /// If the track is already unpublished, this method returns immediately. + /// + pub async fn wait_for_unpublish(&self) { + match self.inner.as_ref() { + DataTrackInner::Local(inner) => inner.wait_for_unpublish().await, + DataTrackInner::Remote(inner) => inner.wait_for_unpublish().await, + } + } +} + +/// Information about a published data track. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct DataTrackInfo { + pub(crate) sid: Arc>, + pub(crate) pub_handle: Handle, + pub(crate) name: String, + pub(crate) uses_e2ee: bool, +} + +impl DataTrackInfo { + /// Unique track identifier assigned by the SFU. + /// + /// This identifier may change if a reconnect occurs. Use [`Self::name`] if a + /// stable identifier is needed. + /// + pub fn sid(&self) -> DataTrackSid { + self.sid.read().unwrap().clone() + } + + /// Name of the track assigned by the publisher. + pub fn name(&self) -> &str { + &self.name + } + + /// Whether or not frames sent on the track use end-to-end encryption. + pub fn uses_e2ee(&self) -> bool { + self.uses_e2ee + } +} + +/// SFU-assigned identifier uniquely identifying a data track. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct DataTrackSid(String); + +#[derive(Debug, Error)] +#[error("Invalid data track SID")] +pub struct DataTrackSidError; + +impl DataTrackSid { + const PREFIX: &str = "DTR_"; +} + +impl TryFrom for DataTrackSid { + type Error = DataTrackSidError; + + fn try_from(raw_id: String) -> Result { + if raw_id.starts_with(Self::PREFIX) { + Ok(Self(raw_id)) + } else { + Err(DataTrackSidError) + } + } +} + +impl From for String { + fn from(id: DataTrackSid) -> Self { + id.0 + } +} + +impl Display for DataTrackSid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +impl fake::Dummy for DataTrackSid { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + const BASE_57_ALPHABET: &[u8; 57] = + b"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + let random_id: String = (0..12) + .map(|_| { + let idx = rng.random_range(0..BASE_57_ALPHABET.len()); + BASE_57_ALPHABET[idx] as char + }) + .collect(); + Self::try_from(format!("{}{}", Self::PREFIX, random_id)).unwrap() + } +} diff --git a/livekit-datatrack/src/utils/bytes.rs b/livekit-datatrack/src/utils/bytes.rs new file mode 100644 index 000000000..6019359a2 --- /dev/null +++ b/livekit-datatrack/src/utils/bytes.rs @@ -0,0 +1,86 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::Bytes; + +/// Extension methods for chunking [`Bytes`] into zero-copy payloads. +pub trait BytesChunkExt { + /// Split into zero-copy chunks of size <= `max_size`. + /// + /// # Panics + /// If `max_size` is equal to zero. + /// + fn into_chunks(self, max_size: usize) -> ChunkIter; +} + +impl BytesChunkExt for Bytes { + fn into_chunks(self, max_size: usize) -> ChunkIter { + assert_ne!(max_size, 0, "Zero chunk size is invalid"); + ChunkIter { source: self, max_size } + } +} + +/// An iterator over chunks of a certain size. +/// +/// Internally, this uses [`Bytes::split_to`], an O(1) operation. +/// +pub struct ChunkIter { + source: Bytes, + max_size: usize, +} + +impl Iterator for ChunkIter { + type Item = Bytes; + + fn next(&mut self) -> Option { + if self.source.is_empty() { + return None; + } + let n = self.max_size.min(self.source.len()); + Some(self.source.split_to(n)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_matrix; + + #[test] + fn test_empty_source() { + let source = Bytes::new(); + let chunks: Vec<_> = source.into_chunks(256).collect(); + assert!(chunks.is_empty()) + } + + #[test_matrix([1, 128, 333], [1, 64, 128, 256, 123])] + fn test_chunks(chunk_size: usize, source_size: usize) { + let source = Bytes::from(vec![0xCC; source_size]); + let chunks: Vec<_> = source.into_chunks(chunk_size).collect(); + + let expected_chunks = (source_size + chunk_size - 1) / chunk_size; + assert_eq!(chunks.len(), expected_chunks); + + // All but last chunk's length match chunks size + assert!(chunks[..chunks.len().saturating_sub(1)].iter().all(|c| c.len() == chunk_size)); + + // Last is either full (divisible) or the remainder. + let expected_last_len = if source_size % chunk_size == 0 { + chunk_size.min(source_size) + } else { + source_size % chunk_size + }; + assert_eq!(chunks.last().unwrap().len(), expected_last_len); + } +} diff --git a/livekit-datatrack/src/utils/counter.rs b/livekit-datatrack/src/utils/counter.rs new file mode 100644 index 000000000..be934a7af --- /dev/null +++ b/livekit-datatrack/src/utils/counter.rs @@ -0,0 +1,53 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A counter that increases monotonically and wraps on overflow. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct Counter(T); + +#[allow(dead_code)] +impl Counter { + pub fn new(start: T) -> Self { + Self(start) + } + + /// Returns the current value. + pub fn get(self) -> T { + self.0 + } + + /// Returns current value, then increments with wrap-around. + pub fn get_then_increment(&mut self) -> T { + let current = self.0; + self.0 = self.0.wrapping_inc(); + current + } +} + +/// A type that supports incrementing with wrap-around. +pub trait WrappingIncrement: Copy { + fn wrapping_inc(self) -> Self; +} + +macro_rules! impl_increment { + ($($t:ty),* $(,)?) => { + $(impl WrappingIncrement for $t { + fn wrapping_inc(self) -> Self { + self.wrapping_add(1) + } + })* + }; +} + +impl_increment!(u8, u16, u32, u64); diff --git a/livekit-datatrack/src/utils/mod.rs b/livekit-datatrack/src/utils/mod.rs new file mode 100644 index 000000000..bc86ba6f8 --- /dev/null +++ b/livekit-datatrack/src/utils/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Utilities for working with [`Bytes::bytes`]. +mod bytes; + +mod counter; + +pub use bytes::*; +pub use counter::*; + +#[cfg(test)] +pub(crate) mod testing; diff --git a/livekit-datatrack/src/utils/testing.rs b/livekit-datatrack/src/utils/testing.rs new file mode 100644 index 000000000..b729e2c5c --- /dev/null +++ b/livekit-datatrack/src/utils/testing.rs @@ -0,0 +1,39 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Drains an output event stream until an event matching `$variant` arrives. +/// Other events are silently skipped. Panics if the variant doesn't arrive +/// within the timeout (default 500ms). +macro_rules! expect_event { + ($output:expr, $variant:path) => { + expect_event!($output, $variant, std::time::Duration::from_millis(500)) + }; + ($output:expr, $variant:path, $timeout:expr) => { + tokio::time::timeout($timeout, async { + loop { + match futures_util::StreamExt::next(&mut $output) + .await + .expect("Stream ended before receiving expected event") + { + $variant(e) => break e, + _ => {} + } + } + }) + .await + .expect(concat!("Timed out waiting for ", stringify!($variant))) + }; +} + +pub(crate) use expect_event;