diff --git a/Cargo.lock b/Cargo.lock index 74fcb8372..cf6e1f17b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cipher", "cpufeatures", ] @@ -72,7 +72,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "getrandom 0.3.4", "once_cell", "serde", @@ -97,7 +97,7 @@ checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", "bitflags 2.10.0", - "cfg-if", + "cfg-if 1.0.4", "libc", ] @@ -377,7 +377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", - "cfg-if", + "cfg-if 1.0.4", "concurrent-queue", "futures-io", "futures-lite 2.6.1", @@ -533,7 +533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", - "cfg-if", + "cfg-if 1.0.4", "libc", "miniz_oxide", "object", @@ -568,6 +568,18 @@ dependencies = [ "serde", ] +[[package]] +name = "basic_data_track" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger 0.11.8", + "futures-util", + "livekit", + "log", + "tokio", +] + [[package]] name = "basic_room" version = "0.1.0" @@ -590,6 +602,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.110", + "which", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -894,6 +929,12 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.4" @@ -989,6 +1030,34 @@ dependencies = [ "error-code", ] +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1113,13 +1182,23 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] @@ -1129,16 +1208,34 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.23.2" @@ -1147,7 +1244,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types", + "core-graphics-types 0.1.3", "foreign-types 0.5.0", "libc", ] @@ -1163,6 +1260,42 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal 0.18.0", + "objc", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1170,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "coreaudio-sys", ] @@ -1180,7 +1313,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" dependencies = [ - "bindgen", + "bindgen 0.72.1", ] [[package]] @@ -1190,7 +1323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ "alsa", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "coreaudio-rs", "dasp_sample", "jni", @@ -1221,7 +1354,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -1388,8 +1521,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]] @@ -1406,24 +1549,48 @@ 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.110", +] + [[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.110", +] + [[package]] name = "dashmap" version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -1451,6 +1618,12 @@ dependencies = [ "powerfmt", ] +[[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" @@ -1519,6 +1692,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[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.110", +] + [[package]] name = "ecolor" version = "0.31.1" @@ -1647,7 +1832,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -1798,6 +1983,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" @@ -1870,6 +2067,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1954,7 +2163,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", @@ -2122,7 +2331,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "libc", "wasi", @@ -2135,7 +2344,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "libc", "r-efi", @@ -2364,7 +2573,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "crunchy", "num-traits", "zerocopy", @@ -2654,7 +2863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "iana-time-zone-haiku", "js-sys", "log", @@ -2855,7 +3064,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -3002,7 +3211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if", + "cfg-if 1.0.4", "combine", "jni-sys", "log", @@ -3091,6 +3300,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lebe" version = "0.5.3" @@ -3109,7 +3324,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "windows-link", ] @@ -3212,6 +3427,7 @@ dependencies = [ "libloading", "libwebrtc", "livekit-api", + "livekit-datatrack", "livekit-protocol", "livekit-runtime", "log", @@ -3220,6 +3436,7 @@ dependencies = [ "semver", "serde", "serde_json", + "test-case", "test-log", "thiserror 1.0.69", "tokio", @@ -3255,6 +3472,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.17", + "tokio", + "tokio-stream", +] + [[package]] name = "livekit-ffi" version = "0.12.44" @@ -3339,6 +3576,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "local_video" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytemuck", + "clap", + "eframe", + "egui", + "egui-wgpu", + "env_logger 0.10.2", + "futures", + "image 0.24.9", + "libwebrtc", + "livekit", + "livekit-api", + "log", + "nokhwa", + "objc2 0.6.3", + "parking_lot", + "tokio", + "webrtc-sys", + "wgpu 25.0.2", + "winit", + "yuv-sys", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -3411,6 +3675,21 @@ dependencies = [ "libc", ] +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + [[package]] name = "metal" version = "0.31.0" @@ -3419,7 +3698,7 @@ checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.10.0", "block", - "core-graphics-types", + "core-graphics-types 0.1.3", "foreign-types 0.5.0", "log", "objc", @@ -3535,6 +3814,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "napi" version = "2.16.17" @@ -3561,7 +3849,7 @@ version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "convert_case", "napi-derive-backend", "proc-macro2", @@ -3669,6 +3957,72 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nokhwa" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cae50786bfa1214ed441f98addbea51ca1b9aaa9e4bf5369cda36654b3efaa" +dependencies = [ + "flume", + "image 0.25.9", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "parking_lot", + "paste", + "thiserror 2.0.17", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd666aaa41d14357817bd9a981773a73c4d00b34d344cfc244e47ebd397b1ec" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de78eb4a2d47a68f490899aa0516070d7a972f853ec2bb374ab53be0bd39b60f" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899799275c93ef69bbe8cb888cf6f8249abe751cbc50be5299105022aec14a1c" +dependencies = [ + "nokhwa-core", + "once_cell", + "windows 0.62.2", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109975552bbd690894f613bce3d408222911e317197c72b2e8b9a1912dc261ae" +dependencies = [ + "bytes", + "image 0.25.9", + "thiserror 2.0.17", +] + [[package]] name = "nom" version = "7.1.3" @@ -3744,6 +4098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -4016,6 +4371,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "object" version = "0.37.3" @@ -4067,7 +4431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.10.0", - "cfg-if", + "cfg-if 1.0.4", "foreign-types 0.3.2", "libc", "once_cell", @@ -4164,7 +4528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "backtrace", - "cfg-if", + "cfg-if 1.0.4", "libc", "petgraph 0.6.5", "redox_syscall 0.5.18", @@ -4238,6 +4602,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4364,7 +4734,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.4", "concurrent-queue", "libc", "log", @@ -4378,7 +4748,7 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "concurrent-queue", "hermit-abi", "pin-project-lite", @@ -4906,7 +5276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.4", "getrandom 0.2.16", "libc", "untrusted", @@ -5207,7 +5577,7 @@ checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", "core-foundation 0.9.4", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", "security-framework-sys", ] @@ -5220,7 +5590,7 @@ checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.10.0", "core-foundation 0.10.1", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", "security-framework-sys", ] @@ -5231,7 +5601,7 @@ version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] @@ -5328,7 +5698,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -5339,7 +5709,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -5518,6 +5888,15 @@ dependencies = [ "hound", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -5674,6 +6053,39 @@ 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.110", +] + +[[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.110", + "test-case-core", +] + [[package]] name = "test-log" version = "0.2.18" @@ -5751,7 +6163,7 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -5807,7 +6219,7 @@ dependencies = [ "arrayref", "arrayvec", "bytemuck", - "cfg-if", + "cfg-if 1.0.4", "log", "tiny-skia-path", ] @@ -5926,6 +6338,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6442,6 +6855,26 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6518,7 +6951,7 @@ version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -6531,7 +6964,7 @@ version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "once_cell", "wasm-bindgen", @@ -6957,7 +7390,7 @@ dependencies = [ "block", "bytemuck", "cfg_aliases", - "core-graphics-types", + "core-graphics-types 0.1.3", "glow", "glutin_wgl_sys", "gpu-alloc", @@ -6967,7 +7400,7 @@ dependencies = [ "libc", "libloading", "log", - "metal", + "metal 0.31.0", "naga 24.0.0", "ndk-sys 0.5.0+25.2.9519653", "objc", @@ -6999,9 +7432,9 @@ dependencies = [ "bitflags 2.10.0", "block", "bytemuck", - "cfg-if", + "cfg-if 1.0.4", "cfg_aliases", - "core-graphics-types", + "core-graphics-types 0.1.3", "glow", "glutin_wgl_sys", "gpu-alloc", @@ -7013,7 +7446,7 @@ dependencies = [ "libc", "libloading", "log", - "metal", + "metal 0.31.0", "naga 25.0.1", "ndk-sys 0.5.0+25.2.9519653", "objc", @@ -7081,6 +7514,18 @@ dependencies = [ "winit", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7132,6 +7577,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -7168,6 +7634,17 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -7218,6 +7695,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -7381,6 +7868,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7577,7 +8073,7 @@ dependencies = [ "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", - "core-graphics", + "core-graphics 0.23.2", "cursor-icon", "dpi", "js-sys", @@ -7724,8 +8220,9 @@ dependencies = [ name = "yuv-sys" version = "0.3.10" dependencies = [ - "bindgen", + "bindgen 0.72.1", "cc", + "pkg-config", "rayon", "regex", ] diff --git a/Cargo.toml b/Cargo.toml index 478544dd9..edebc6abf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "livekit-uniffi", "livekit-ffi-node-bindings", "livekit-runtime", + "livekit-datatrack", "libwebrtc", "soxr-sys", "yuv-sys", @@ -19,6 +20,7 @@ members = [ "examples/api", "examples/basic_room", "examples/basic_text_stream", + "examples/basic_data_track", "examples/encrypted_text_stream", "examples/local_audio", "examples/local_video", @@ -40,6 +42,7 @@ livekit-api = { version = "0.4.12", path = "livekit-api" } livekit-ffi = { version = "0.12.44", path = "livekit-ffi" } livekit-protocol = { version = "0.6.0", path = "livekit-protocol" } livekit-runtime = { version = "0.4.0", path = "livekit-runtime" } +livekit-datatrack = { version = "0.1.0", path = "livekit-datatrack" } soxr-sys = { version = "0.1.1", path = "soxr-sys" } webrtc-sys = { version = "0.3.21", path = "webrtc-sys" } webrtc-sys-build = { version = "0.3.12", path = "webrtc-sys/build" } diff --git a/examples/basic_data_track/Cargo.toml b/examples/basic_data_track/Cargo.toml new file mode 100644 index 000000000..c52a16431 --- /dev/null +++ b/examples/basic_data_track/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "basic_data_track" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +tokio = { version = "1", features = ["full"] } +livekit = { workspace = true, features = ["rustls-tls-native-roots"] } +futures-util = { version = "0.3", default-features = false, features = ["sink"] } +anyhow = "1.0.100" +log = "0.4.26" +env_logger = "0.11.7" + +[[bin]] +name = "publisher" +path = "src/publisher.rs" + +[[bin]] +name = "subscriber" +path = "src/subscriber.rs" \ No newline at end of file diff --git a/examples/basic_data_track/README.md b/examples/basic_data_track/README.md new file mode 100644 index 000000000..537d029ea --- /dev/null +++ b/examples/basic_data_track/README.md @@ -0,0 +1,21 @@ +# Basic Data Track + +Simple example of publishing and subscribing to a data track. + +## Usage + +1. Run the publisher: + +```sh +export LIVEKIT_URL="..." +export LIVEKIT_TOKEN="" +cargo run --bin publisher +``` + +2. In a second terminal, run the subscriber: + +```sh +export LIVEKIT_URL="..." +export LIVEKIT_TOKEN="" +cargo run --bin subscriber +``` diff --git a/examples/basic_data_track/src/publisher.rs b/examples/basic_data_track/src/publisher.rs new file mode 100644 index 000000000..e0600947e --- /dev/null +++ b/examples/basic_data_track/src/publisher.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use livekit::prelude::*; +use std::{env, time::Duration}; +use tokio::{signal, time}; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); + let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); + + let (room, _) = Room::connect(&url, &token, RoomOptions::default()).await?; + + let track = room.local_participant().publish_data_track("my_sensor_data").await?; + + tokio::select! { + _ = push_frames(track) => {} + _ = signal::ctrl_c() => {} + } + Ok(()) +} + +async fn read_sensor() -> Vec { + // Dynamically read some sensor data... + vec![0xFA; 256] +} + +async fn push_frames(track: LocalDataTrack) { + loop { + log::info!("Pushing frame"); + + let frame = DataTrackFrame::new(read_sensor().await) + .with_user_timestamp_now(); + + track.try_push(frame).inspect_err(|err| println!("Failed to push frame: {}", err)).ok(); + time::sleep(Duration::from_millis(500)).await + } +} diff --git a/examples/basic_data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs new file mode 100644 index 000000000..fc489133a --- /dev/null +++ b/examples/basic_data_track/src/subscriber.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use futures_util::StreamExt; +use livekit::prelude::*; +use std::env; +use tokio::{signal, sync::mpsc::UnboundedReceiver}; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); + let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); + + let (_, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; + + tokio::select! { + _ = handle_first_publication(rx) => {} + _ = signal::ctrl_c() => {} + } + Ok(()) +} + +/// Subscribe to the first data track published. +async fn handle_first_publication(mut rx: UnboundedReceiver) -> Result<()> { + log::info!("Waiting for publication…"); + while let Some(event) = rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + subscribe(track).await? + } + Ok(()) +} + +/// Subscribes to the given data track and logs received frames. +async fn subscribe(track: RemoteDataTrack) -> Result<()> { + log::info!( + "Subscribing to '{}' published by '{}'", + track.info().name(), + track.publisher_identity() + ); + let mut stream = track.subscribe().await?; + while let Some(frame) = stream.next().await { + log::info!("Received frame ({} bytes)", frame.payload().len()); + + if let Some(duration) = frame.duration_since_timestamp() { + log::info!("Latency: {:?}", duration); + } + } + log::info!("Unsubscribed"); + Ok(()) +} diff --git a/livekit-api/src/services/connector.rs b/livekit-api/src/services/connector.rs index 551e689d8..02eec319b 100644 --- a/livekit-api/src/services/connector.rs +++ b/livekit-api/src/services/connector.rs @@ -164,6 +164,7 @@ impl ConnectorClient { proto::DisconnectWhatsAppCallRequest { whatsapp_call_id: call_id.into(), whatsapp_api_key: api_key.into(), + ..Default::default() }, self.base .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?, diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml new file mode 100644 index 000000000..b9419feea --- /dev/null +++ b/livekit-datatrack/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "livekit-datatrack" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/livekit/rust-sdks" +readme = "README.md" + +[dependencies] +livekit-protocol = { workspace = true } +livekit-runtime = { workspace = true } +log = { version = "0.4.28" } +thiserror = "2.0.17" +tokio = { version = "1.48.0", default-features = false, features = ["sync"] } +futures-util = { version = "0.3", default-features = false, features = ["sink"] } +futures-core = "0.3.31" +bytes = "1.10.1" +from_variants = "1.0.2" +tokio-stream = { version = "0.1.17", features = ["sync"] } +anyhow = "1.0.100" # For internal error handling only +rand = "0.9.2" + +[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/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..86d52741e --- /dev/null +++ b/livekit-datatrack/src/frame.rs @@ -0,0 +1,122 @@ +// 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 { + SystemTime::now() + .duration_since(UNIX_EPOCH + Duration::from_millis(self.user_timestamp?)) + .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..01e487fd4 --- /dev/null +++ b/livekit-datatrack/src/local/events.rs @@ -0,0 +1,131 @@ +// 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::sync::Arc; +use crate::{ + api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError}, + packet::Handle, +}; +use bytes::Bytes; +use from_variants::FromVariants; +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), + /// 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..6cb7f6517 --- /dev/null +++ b/livekit-datatrack/src/local/manager.rs @@ -0,0 +1,464 @@ +// 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 decryption_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(4); // TODO: tune buffer size + let (event_out_tx, event_out_rx) = mpsc::channel(4); + + let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; + let manager = Manager { + encryption_provider: options.decryption_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::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 { + log::warn!("No descriptor for {}", event.handle); + return; + }; + let Descriptor::Pending(result_tx) = descriptor else { + log::warn!("Track {} already active", event.handle); + return; + }; + + if result_tx.is_closed() { + return; + } + let result = event.result.map(|track_info| self.create_local_track(track_info)); + _ = result_tx.send(result); + } + + 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(4); // TODO: tune + let (published_tx, published_rx) = watch::channel(true); + + let track_task = TrackTask { + info: info.clone(), + pipeline, + published_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(), + published_tx: published_tx.clone(), + task_handle, + }, + ); + + let inner = LocalTrackInner { frame_tx, published_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 { published_tx, .. } = descriptor else { + return; + }; + if !*published_tx.borrow() { + _ = published_tx.send(false); + } + } + + /// 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 { published_tx, task_handle, .. } => { + _ = published_tx.send(false); + task_handle.await; + } + } + } + } +} + +/// Task for an individual published data track. +struct TrackTask { + info: Arc, + pipeline: Pipeline, + published_rx: watch::Receiver, + frame_rx: mpsc::Receiver, + event_in_tx: mpsc::Sender, + event_out_tx: mpsc::Sender, +} + +impl TrackTask { + async fn run(mut self) { + log::debug!("Track task started: sid={}", self.info.sid); + + let mut is_published = *self.published_rx.borrow(); + while is_published { + tokio::select! { + _ = self.published_rx.changed() => { + is_published = *self.published_rx.borrow(); + } + Some(frame) = self.frame_rx.recv() => 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={}", self.info.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 packet 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, + published_tx: watch::Sender, + task_handle: livekit_runtime::JoinHandle<()>, + }, +} + +/// Channel for sending [`InputEvent`]s to [`Manager`]. +#[derive(Debug, Clone)] +pub struct ManagerInput { + event_in_tx: mpsc::Sender, +} + +impl ManagerInput { + /// 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, packet::Packet}; + use fake::{Fake, Faker}; + use futures_util::StreamExt; + use livekit_runtime::{sleep, timeout}; + + #[tokio::test] + async fn test_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); + + 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 { decryption_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: track_sid.clone(), + 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(); + } +} diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs new file mode 100644 index 000000000..667290241 --- /dev/null +++ b/livekit-datatrack/src/local/mod.rs @@ -0,0 +1,237 @@ +// 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 proto; +pub(crate) mod events; +pub(crate) mod manager; + +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)] +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> { + if !self.is_published() { + return Err(PushFrameError::new(frame, PushFrameErrorReason::TrackUnpublished)); + } + self.inner().frame_tx.try_send(frame).map_err(|err| { + PushFrameError::new(err.into_inner(), PushFrameErrorReason::Dropped) + }) + } + + /// Unpublishes the track. + pub fn unpublish(self) { + self.inner().local_unpublish(); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct LocalTrackInner { + pub frame_tx: mpsc::Sender, + pub published_tx: watch::Sender, +} + +impl LocalTrackInner { + fn local_unpublish(&self) { + _ = self.published_tx.send(false); + } + + pub fn published_rx(&self) -> watch::Receiver { + self.published_tx.subscribe() + } +} + +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, + + /// 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. + Dropped, +} + +impl fmt::Display for PushFrameErrorReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TrackUnpublished => write!(f, "track unpublished"), + Self::Dropped => write!(f, "dropped"), + } + } +} diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs new file mode 100644 index 000000000..cf3f106f8 --- /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> { + // TODO: consider using default + let header = Header { + marker: FrameMarker::Inter, + track_handle: self.handle, + sequence: 0, + frame_number: self.frame_number.get_then_increment(), + 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)? + } + + let packet_payloads: Vec<_> = frame.payload.into_chunks(max_payload_size).collect(); + let packet_count = packet_payloads.len(); + let packets = packet_payloads + .into_iter() + .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..ae74eed79 --- /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.into()); + }; + + 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 fake::{Fake, Faker}; + use bytes::Bytes; + + #[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..0e78a4728 --- /dev/null +++ b/livekit-datatrack/src/local/proto.rs @@ -0,0 +1,204 @@ +// 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; + +// 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, 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, + _ => 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, "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..dfc5651cd --- /dev/null +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -0,0 +1,275 @@ +// 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 + 1) as usize; + 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) + } +} + +impl Extensions { + fn deserialize(mut raw: impl Buf) -> Result { + let mut extensions = Self::default(); + while raw.remaining() >= 4 { + let tag = raw.get_u16(); + let len = raw.get_u16() as usize; + if tag == EXT_TAG_PADDING { + // Skip padding + continue; + } + match tag { + E2eeExt::TAG => { + if raw.remaining() < E2eeExt::LEN { + Err(DeserializeError::MalformedExt(tag))? + } + let key_index = raw.get_u8(); + let mut iv = [0u8; 12]; + raw.copy_to_slice(&mut iv); + extensions.e2ee = E2eeExt { key_index, iv }.into(); + } + UserTimestampExt::TAG => { + if raw.remaining() < UserTimestampExt::LEN { + Err(DeserializeError::MalformedExt(tag))? + } + extensions.user_timestamp = UserTimestampExt(raw.get_u64()).into() + } + _ => { + // Skip over unknown extensions (forward compatible). + if raw.remaining() < len { + Err(DeserializeError::MalformedExt(tag))? + } + raw.advance(len); + continue; + } + } + } + Ok(extensions) + } +} + +#[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(4); // Extension words + + raw.put_u16(1); // ID 1 + raw.put_u16(12); // Length 12 + raw.put_u8(0xFA); // Key index + raw.put_bytes(0x3C, 12); // IV + raw.put_bytes(0, 3); // 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_u16(2); + raw.put_u16(7); + raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp + + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!( + packet.header.extensions.user_timestamp, + UserTimestampExt(0x4411221111118811).into() + ); + } + + #[test] + fn test_ext_unknown() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(1); // Extension words + + raw.put_u16(8); // ID 8 (unknown) + raw.put_bytes(0, 6); + 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..e4d82913f --- /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 = u16; + +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..88ef700be --- /dev/null +++ b/livekit-datatrack/src/packet/handle.rs @@ -0,0 +1,89 @@ +// 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. +#[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..0564fee29 --- /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 = 4; + pub const EXT_TAG_PADDING: u16 = 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); + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs new file mode 100644 index 000000000..5ca960dc5 --- /dev/null +++ b/livekit-datatrack/src/packet/serialize.rs @@ -0,0 +1,241 @@ +// 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 { + 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_u16(Self::TAG); + buf.put_u16(Self::LEN as u16 - 1); + buf.put_u8(self.key_index); + buf.put_slice(&self.iv); + } +} + +impl UserTimestampExt { + fn serialize_into(self, buf: &mut impl BufMut) { + buf.put_u16(Self::TAG); + buf.put_u16(Self::LEN as u16 - 1); + 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, 29); + assert_eq!(metrics.ext_words, 8); + assert_eq!(metrics.padding_len, 3); + } + + #[test] + fn test_serialized_length() { + let packet = packet(); + assert_eq!(packet.serialized_len(), 1070); + assert_eq!(packet.header.serialized_len(), 46); + assert_eq!(packet.header.extensions.serialized_len(), 29); + } + + #[test] + fn test_serialize() { + let mut buf = packet().serialize().try_into_mut().unwrap(); + assert_eq!(buf.len(), 1070); + + // 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(), 7); // Extension words + + // E2EE extension + assert_eq!(buf.get_u16(), 1); // ID 1, + assert_eq!(buf.get_u16(), 12); // Length 12 + 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_u16(), 2); // ID 2 + assert_eq!(buf.get_u16(), 7); // Length 7 + 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..fc4710674 --- /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 payload_len = packet.payload.len(); + + let partial = PartialFrame { + frame_number: packet.header.frame_number, + start_sequence, + extensions: packet.header.extensions, + payloads: BTreeMap::from([(start_sequence, packet.payload)]), + payload_len, + }; + 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(); + } + + partial.payload_len += packet.payload.len(); + partial.payloads.insert(packet.header.sequence, packet.payload); + + 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 mut sequence = partial.start_sequence; + let mut payload = BytesMut::with_capacity(partial.payload_len); + + 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 - partial.start_sequence + 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, + /// Sum of payload lengths. + payload_len: usize, +} + +/// 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 fake::{Fake, Faker}; + use test_case::test_case; + use crate::utils::Counter; + + #[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 += 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 += 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 += 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..d5fae9be5 --- /dev/null +++ b/livekit-datatrack/src/remote/events.rs @@ -0,0 +1,126 @@ +// 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, RemoteDataTrack, SubscribeError}, + 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), + /// 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. + /// + TrackAvailable(RemoteDataTrack), +} + +// MARK: - Input events + +/// Result of a [`SubscribeRequest`]. +pub(super) type SubscribeResult = Result, SubscribeError>; + +/// 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, + /// 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, +} diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs new file mode 100644 index 000000000..11ef250b9 --- /dev/null +++ b/livekit-datatrack/src/remote/manager.rs @@ -0,0 +1,560 @@ +// 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, InternalError, SubscribeError}, + 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 encryption_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(4); // TODO: tune buffer size + let (event_out_tx, event_out_rx) = mpsc::channel(4); + + let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; + let manager = Manager { + decryption_provider: options.encryption_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 = + SubscribeError::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.clone(), subscribe: true }; + _ = self.event_out_tx.send(update_event.into()).await; + descriptor.subscription = + SubscriptionState::Pending { result_txs: vec![event.result_tx] }; + // 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 sids_in_update = HashSet::new(); + + // Detect published track + for (publisher_identity, tracks) in event.updates { + for info in tracks { + sids_in_update.insert(info.sid.clone()); + if self.descriptors.contains_key(&info.sid) { + continue; + } + self.handle_track_published(publisher_identity.clone(), info).await; + } + } + + // Detect unpublished tracks + let unpublished_sids: Vec<_> = + self.descriptors.keys().filter(|sid| !sids_in_update.contains(*sid)).cloned().collect(); + for sid in unpublished_sids { + self.handle_track_unpublished(sid.clone()); + } + } + + async fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { + if self.descriptors.contains_key(&info.sid) { + log::error!("Existing descriptor for track {}", info.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(descriptor.info.sid.clone(), 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(track.into()).await; + } + + 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); + } + + 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 = 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. + *sub_handle = assigned_handle; + self.sub_handles.insert(assigned_handle, sid); + return; + } + SubscriptionState::Pending { result_txs } => { + // Handle assigned for pending subscription, transition to active. + mem::take(result_txs) + } + }; + + let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune + let (frame_tx, frame_rx) = broadcast::channel(4); + + 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(SubscribeError::Disconnected)); + } + } + SubscriptionState::Active { task_handle, .. } => task_handle.await, + } + } + } +} + +/// 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 { result_txs: Vec> }, + /// 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!("Task started: sid={}", self.info.sid); + + 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.clone() }; + _ = 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!("Task ended: sid={}", self.info.sid); + } + + 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, +} + +impl ManagerInput { + /// 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 fake::{Fake, Faker}; + use futures_util::{future::join, StreamExt}; + use std::{collections::HashMap, time::Duration}; + use test_case::test_case; + use tokio::time; + + #[tokio::test] + async fn test_manager_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); + + 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.clone(); + 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 { encryption_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: track_sid.clone(), + 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::TrackAvailable(track) => return track, + _ => continue, + } + } + panic!("No track received"); + }; + + let track = wait_for_track.await; + 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(); + } +} diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs new file mode 100644 index 000000000..8b78f1cc5 --- /dev/null +++ b/livekit-datatrack/src/remote/mod.rs @@ -0,0 +1,128 @@ +// 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 anyhow::anyhow; +use futures_util::StreamExt; +use livekit_runtime::timeout; +use events::{InputEvent, SubscribeRequest}; +use std::{marker::PhantomData, sync::Arc, time::Duration}; +use thiserror::Error; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio_stream::{wrappers::BroadcastStream, Stream}; + +pub(crate) mod proto; +pub(crate) mod events; +pub(crate) mod manager; + +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 to receive frames. + /// + /// # Returns + /// + /// A stream that yields [`DataTrackFrame`]s as they arrive. + /// + /// # 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, SubscribeError> { + let (result_tx, result_rx) = oneshot::channel(); + let subscribe_event = SubscribeRequest { sid: self.info.sid.clone(), result_tx }; + self.inner() + .event_in_tx + .upgrade() + .ok_or(SubscribeError::Disconnected)? + .send_timeout(subscribe_event.into(), Duration::from_millis(50)) + .await + .map_err(|_| { + SubscribeError::Internal(anyhow!("Failed to send subscribe event").into()) + })?; + + // TODO: standardize timeout + let frame_rx = timeout(Duration::from_secs(10), result_rx) + .await + .map_err(|_| SubscribeError::Timeout)? + .map_err(|_| SubscribeError::Disconnected)??; + + let frame_stream = + BroadcastStream::new(frame_rx).filter_map(|result| async move { result.ok() }); + Ok(Box::pin(frame_stream)) + } + + /// Identity of the participant who published the track. + pub fn publisher_identity(&self) -> &str { + &self.inner().publisher_identity + } +} + +#[derive(Debug, Clone)] +pub(crate) struct RemoteTrackInner { + publisher_identity: Arc, + published_rx: watch::Receiver, + event_in_tx: mpsc::WeakSender, +} + +impl RemoteTrackInner { + pub fn published_rx(&self) -> watch::Receiver { + self.published_rx.clone() + } +} + +#[derive(Debug, Error)] +pub enum SubscribeError { + #[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), +} diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs new file mode 100644 index 000000000..8e6d443f0 --- /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 Some(frame) = self.depacketize(packet) else { return None }; + let Some(frame) = self.decrypt_if_needed(frame) else { return None }; + 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..c3c505bc6 --- /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, "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..33dc34893 --- /dev/null +++ b/livekit-datatrack/src/track.rs @@ -0,0 +1,145 @@ +// 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}; +use tokio::sync::watch; +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 { + let published_rx = self.published_rx(); + let published = *published_rx.borrow(); + 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) { + let mut published_rx = self.published_rx(); + if !*published_rx.borrow() { + // Already unpublished + return; + } + _ = published_rx.wait_for(|is_published| !*is_published).await; + } + + fn published_rx(&self) -> watch::Receiver { + match self.inner.as_ref() { + DataTrackInner::Local(inner) => inner.published_rx(), + DataTrackInner::Remote(inner) => inner.published_rx(), + } + } +} + +/// Information about a published data track. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(fake::Dummy))] +pub struct DataTrackInfo { + pub(crate) sid: DataTrackSid, + pub(crate) pub_handle: Handle, + pub(crate) name: String, + pub(crate) uses_e2ee: bool, +} + +impl DataTrackInfo { + /// Unique track identifier. + pub fn sid(&self) -> &DataTrackSid { + &self.sid + } + /// 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..de7957d22 --- /dev/null +++ b/livekit-datatrack/src/utils/mod.rs @@ -0,0 +1,21 @@ +// 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::*; diff --git a/livekit-ffi-node-bindings/src/proto/participant_pb.ts b/livekit-ffi-node-bindings/src/proto/participant_pb.ts index 90d956f9f..ef9eb0ce9 100644 --- a/livekit-ffi-node-bindings/src/proto/participant_pb.ts +++ b/livekit-ffi-node-bindings/src/proto/participant_pb.ts @@ -55,6 +55,11 @@ export enum ParticipantKind { * @generated from enum value: PARTICIPANT_KIND_CONNECTOR = 5; */ CONNECTOR = 5, + + /** + * @generated from enum value: PARTICIPANT_KIND_BRIDGE = 6; + */ + BRIDGE = 6, } // Retrieve enum metadata with: proto2.getEnumType(ParticipantKind) proto2.util.setEnumType(ParticipantKind, "livekit.proto.ParticipantKind", [ @@ -64,6 +69,7 @@ proto2.util.setEnumType(ParticipantKind, "livekit.proto.ParticipantKind", [ { no: 3, name: "PARTICIPANT_KIND_SIP" }, { no: 4, name: "PARTICIPANT_KIND_AGENT" }, { no: 5, name: "PARTICIPANT_KIND_CONNECTOR" }, + { no: 6, name: "PARTICIPANT_KIND_BRIDGE" }, ]); /** @@ -89,6 +95,11 @@ export enum ParticipantKindDetail { * @generated from enum value: PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO = 3; */ CONNECTOR_TWILIO = 3, + + /** + * @generated from enum value: PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP = 4; + */ + BRIDGE_RTSP = 4, } // Retrieve enum metadata with: proto2.getEnumType(ParticipantKindDetail) proto2.util.setEnumType(ParticipantKindDetail, "livekit.proto.ParticipantKindDetail", [ @@ -96,6 +107,7 @@ proto2.util.setEnumType(ParticipantKindDetail, "livekit.proto.ParticipantKindDet { no: 1, name: "PARTICIPANT_KIND_DETAIL_FORWARDED" }, { no: 2, name: "PARTICIPANT_KIND_DETAIL_CONNECTOR_WHATSAPP" }, { no: 3, name: "PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO" }, + { no: 4, name: "PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP" }, ]); /** diff --git a/livekit-ffi/protocol/participant.proto b/livekit-ffi/protocol/participant.proto index 589c508cf..36d1ecc27 100644 --- a/livekit-ffi/protocol/participant.proto +++ b/livekit-ffi/protocol/participant.proto @@ -44,6 +44,7 @@ enum ParticipantKind { PARTICIPANT_KIND_SIP = 3; PARTICIPANT_KIND_AGENT = 4; PARTICIPANT_KIND_CONNECTOR = 5; + PARTICIPANT_KIND_BRIDGE = 6; } enum ParticipantKindDetail { @@ -51,6 +52,7 @@ enum ParticipantKindDetail { PARTICIPANT_KIND_DETAIL_FORWARDED = 1; PARTICIPANT_KIND_DETAIL_CONNECTOR_WHATSAPP = 2; PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO = 3; + PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP = 4; } enum DisconnectReason { diff --git a/livekit-ffi/src/conversion/participant.rs b/livekit-ffi/src/conversion/participant.rs index 89cca3685..69af59b89 100644 --- a/livekit-ffi/src/conversion/participant.rs +++ b/livekit-ffi/src/conversion/participant.rs @@ -58,6 +58,7 @@ impl From for proto::ParticipantKind { ParticipantKind::Egress => proto::ParticipantKind::Egress, ParticipantKind::Agent => proto::ParticipantKind::Agent, ParticipantKind::Connector => proto::ParticipantKind::Connector, + ParticipantKind::Bridge => proto::ParticipantKind::Bridge } } } @@ -71,6 +72,7 @@ impl From for proto::ParticipantKindDetail { proto::ParticipantKindDetail::ConnectorWhatsapp } ParticipantKindDetail::ConnectorTwilio => proto::ParticipantKindDetail::ConnectorTwilio, + ParticipantKindDetail::BridgeRtsp => proto::ParticipantKindDetail::BridgeRtsp } } } diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index fcc48786b..21f690495 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit fcc48786b42607b8ba87e840a5c1d39e00b5f4e9 +Subproject commit 21f690495229eb17470b40015a2d579fb53f7056 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index da058ae5a..4b852e825 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -357,6 +357,8 @@ pub struct ParticipantInfo { pub disconnect_reason: i32, #[prost(enumeration="participant_info::KindDetail", repeated, tag="18")] pub kind_details: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="19")] + pub data_tracks: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `ParticipantInfo`. pub mod participant_info { @@ -593,6 +595,38 @@ pub struct TrackInfo { #[prost(enumeration="BackupCodecPolicy", tag="20")] pub backup_codec_policy: i32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackInfo { + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + #[prost(uint32, tag="1")] + pub pub_handle: u32, + /// Server-assigned track identifier. + #[prost(string, tag="2")] + pub sid: ::prost::alloc::string::String, + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + #[prost(string, tag="3")] + pub name: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on packet payloads. + #[prost(enumeration="encryption::Type", tag="4")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackExtensionParticipantSid { + #[prost(enumeration="DataTrackExtensionId", tag="1")] + pub id: i32, + #[prost(string, tag="2")] + pub participant_sid: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscriptionOptions { + /// Rate in frames per second (FPS) the subscriber wants to receive frames at. + /// If omitted, the subscriber defaults to the publisher's fps + #[prost(uint32, optional, tag="1")] + pub target_fps: ::core::option::Option, +} /// provide information about available spatial layers #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1681,6 +1715,32 @@ impl TrackSource { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum DataTrackExtensionId { + DteiInvalid = 0, + DteiParticipantSid = 1, +} +impl DataTrackExtensionId { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + DataTrackExtensionId::DteiInvalid => "DTEI_INVALID", + DataTrackExtensionId::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DTEI_INVALID" => Some(Self::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Some(Self::DteiParticipantSid), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum VideoQuality { Low = 0, Medium = 1, @@ -3044,7 +3104,7 @@ impl EgressSourceType { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalRequest { - #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18")] + #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3103,12 +3163,21 @@ pub mod signal_request { /// Update local video track settings #[prost(message, tag="18")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + /// Publish a data track + #[prost(message, tag="19")] + PublishDataTrackRequest(super::PublishDataTrackRequest), + /// Unpublish a data track + #[prost(message, tag="20")] + UnpublishDataTrackRequest(super::UnpublishDataTrackRequest), + /// Update subscription state for one or more data tracks + #[prost(message, tag="21")] + UpdateDataSubscription(super::UpdateDataSubscription), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalResponse { - #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26")] + #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3194,6 +3263,15 @@ pub mod signal_response { /// when audio subscription changes, used to enable simulcasting of audio codecs based on subscriptions #[prost(message, tag="26")] SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), + /// Sent in response to `PublishDataTrackRequest`. + #[prost(message, tag="27")] + PublishDataTrackResponse(super::PublishDataTrackResponse), + /// Sent in response to `UnpublishDataTrackRequest` or SFU-initiated unpublish. + #[prost(message, tag="28")] + UnpublishDataTrackResponse(super::UnpublishDataTrackResponse), + /// Sent to data track subscribers to provide mapping from track SIDs to handles. + #[prost(message, tag="29")] + DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3260,6 +3338,62 @@ pub struct AddTrackRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishDataTrackRequest { + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + /// This must be non-zero and unique for each data track published by the publisher. + #[prost(uint32, tag="1")] + pub pub_handle: u32, + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + /// This must be non-empty and no longer than 256 characters. + #[prost(string, tag="2")] + pub name: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on frame payloads. + #[prost(enumeration="encryption::Type", tag="3")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishDataTrackResponse { + /// Information about the published track. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackRequest { + /// Publisher handle of the track to unpublish. + #[prost(uint32, tag="1")] + pub pub_handle: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackResponse { + /// Information about the unpublished track. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscriberHandles { + /// Maps handles from incoming packets to the track SIDs that the packets belong to. + #[prost(map="uint32, message", tag="1")] + pub sub_handles: ::std::collections::HashMap, +} +/// Nested message and enum types in `DataTrackSubscriberHandles`. +pub mod data_track_subscriber_handles { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct PublishedDataTrack { + #[prost(string, tag="1")] + pub publisher_identity: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub publisher_sid: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub track_sid: ::prost::alloc::string::String, + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrickleRequest { #[prost(string, tag="1")] pub candidate_init: ::prost::alloc::string::String, @@ -3375,6 +3509,27 @@ pub struct UpdateSubscription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateDataSubscription { + #[prost(message, repeated, tag="1")] + pub updates: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `UpdateDataSubscription`. +pub mod update_data_subscription { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct Update { + #[prost(string, tag="1")] + pub track_sid: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub subscribe: bool, + /// Options to apply when initially subscribing or updating an existing subscription. + /// When unsubscribing, this field is ignored. + #[prost(message, optional, tag="3")] + pub options: ::core::option::Option, + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateTrackSettings { #[prost(string, repeated, tag="1")] pub track_sids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, @@ -3651,6 +3806,8 @@ pub struct SyncState { pub track_sids_disabled: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag="7")] pub datachannel_receive_states: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="8")] + pub publish_data_tracks: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3762,7 +3919,7 @@ pub struct RequestResponse { pub reason: i32, #[prost(string, tag="3")] pub message: ::prost::alloc::string::String, - #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9")] + #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9, 10, 11")] pub request: ::core::option::Option, } /// Nested message and enum types in `RequestResponse`. @@ -3777,6 +3934,10 @@ pub mod request_response { Queued = 4, UnsupportedType = 5, UnclassifiedError = 6, + InvalidHandle = 7, + InvalidName = 8, + DuplicateHandle = 9, + DuplicateName = 10, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3792,6 +3953,10 @@ pub mod request_response { Reason::Queued => "QUEUED", Reason::UnsupportedType => "UNSUPPORTED_TYPE", Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", + Reason::InvalidHandle => "INVALID_HANDLE", + Reason::InvalidName => "INVALID_NAME", + Reason::DuplicateHandle => "DUPLICATE_HANDLE", + Reason::DuplicateName => "DUPLICATE_NAME", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3804,6 +3969,10 @@ pub mod request_response { "QUEUED" => Some(Self::Queued), "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), + "INVALID_HANDLE" => Some(Self::InvalidHandle), + "INVALID_NAME" => Some(Self::InvalidName), + "DUPLICATE_HANDLE" => Some(Self::DuplicateHandle), + "DUPLICATE_NAME" => Some(Self::DuplicateName), _ => None, } } @@ -3823,6 +3992,10 @@ pub mod request_response { UpdateAudioTrack(super::UpdateLocalAudioTrack), #[prost(message, tag="9")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + #[prost(message, tag="10")] + PublishDataTrack(super::PublishDataTrackRequest), + #[prost(message, tag="11")] + UnpublishDataTrack(super::UnpublishDataTrackRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index ae66f2b6c..1b4660704 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -8157,6 +8157,672 @@ impl<'de> serde::Deserialize<'de> for data_stream::Trailer { deserializer.deserialize_struct("livekit.DataStream.Trailer", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataTrackExtensionId { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::DteiInvalid => "DTEI_INVALID", + Self::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for DataTrackExtensionId { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "DTEI_INVALID", + "DTEI_PARTICIPANT_SID", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackExtensionId; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "DTEI_INVALID" => Ok(DataTrackExtensionId::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Ok(DataTrackExtensionId::DteiParticipantSid), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackExtensionParticipantSid { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.id != 0 { + len += 1; + } + if !self.participant_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackExtensionParticipantSid", len)?; + if self.id != 0 { + let v = DataTrackExtensionId::try_from(self.id) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.id)))?; + struct_ser.serialize_field("id", &v)?; + } + if !self.participant_sid.is_empty() { + struct_ser.serialize_field("participantSid", &self.participant_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "participant_sid", + "participantSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + ParticipantSid, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "id" => Ok(GeneratedField::Id), + "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackExtensionParticipantSid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackExtensionParticipantSid") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut participant_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value::()? as i32); + } + GeneratedField::ParticipantSid => { + if participant_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("participantSid")); + } + participant_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackExtensionParticipantSid { + id: id__.unwrap_or_default(), + participant_sid: participant_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackInfo { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pub_handle != 0 { + len += 1; + } + if !self.sid.is_empty() { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + if self.encryption != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; + } + if !self.sid.is_empty() { + struct_ser.serialize_field("sid", &self.sid)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackInfo { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pub_handle", + "pubHandle", + "sid", + "name", + "encryption", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PubHandle, + Sid, + Name, + Encryption, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), + "sid" => Ok(GeneratedField::Sid), + "name" => Ok(GeneratedField::Name), + "encryption" => Ok(GeneratedField::Encryption), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pub_handle__ = None; + let mut sid__ = None; + let mut name__ = None; + let mut encryption__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); + } + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Sid => { + if sid__.is_some() { + return Err(serde::de::Error::duplicate_field("sid")); + } + sid__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackInfo { + pub_handle: pub_handle__.unwrap_or_default(), + sid: sid__.unwrap_or_default(), + name: name__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSubscriberHandles { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.sub_handles.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles", len)?; + if !self.sub_handles.is_empty() { + struct_ser.serialize_field("subHandles", &self.sub_handles)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "sub_handles", + "subHandles", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + SubHandles, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "subHandles" | "sub_handles" => Ok(GeneratedField::SubHandles), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSubscriberHandles; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSubscriberHandles") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut sub_handles__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::SubHandles => { + if sub_handles__.is_some() { + return Err(serde::de::Error::duplicate_field("subHandles")); + } + sub_handles__ = Some( + map_.next_value::, _>>()? + .into_iter().map(|(k,v)| (k.0, v)).collect() + ); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackSubscriberHandles { + sub_handles: sub_handles__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for data_track_subscriber_handles::PublishedDataTrack { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.publisher_identity.is_empty() { + len += 1; + } + if !self.publisher_sid.is_empty() { + len += 1; + } + if !self.track_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", len)?; + if !self.publisher_identity.is_empty() { + struct_ser.serialize_field("publisherIdentity", &self.publisher_identity)?; + } + if !self.publisher_sid.is_empty() { + struct_ser.serialize_field("publisherSid", &self.publisher_sid)?; + } + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for data_track_subscriber_handles::PublishedDataTrack { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "publisher_identity", + "publisherIdentity", + "publisher_sid", + "publisherSid", + "track_sid", + "trackSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PublisherIdentity, + PublisherSid, + TrackSid, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "publisherIdentity" | "publisher_identity" => Ok(GeneratedField::PublisherIdentity), + "publisherSid" | "publisher_sid" => Ok(GeneratedField::PublisherSid), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = data_track_subscriber_handles::PublishedDataTrack; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSubscriberHandles.PublishedDataTrack") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut publisher_identity__ = None; + let mut publisher_sid__ = None; + let mut track_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PublisherIdentity => { + if publisher_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherIdentity")); + } + publisher_identity__ = Some(map_.next_value()?); + } + GeneratedField::PublisherSid => { + if publisher_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherSid")); + } + publisher_sid__ = Some(map_.next_value()?); + } + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(data_track_subscriber_handles::PublishedDataTrack { + publisher_identity: publisher_identity__.unwrap_or_default(), + publisher_sid: publisher_sid__.unwrap_or_default(), + track_sid: track_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSubscriptionOptions { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.target_fps.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriptionOptions", len)?; + if let Some(v) = self.target_fps.as_ref() { + struct_ser.serialize_field("targetFps", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSubscriptionOptions { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "target_fps", + "targetFps", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TargetFps, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "targetFps" | "target_fps" => Ok(GeneratedField::TargetFps), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSubscriptionOptions; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSubscriptionOptions") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut target_fps__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TargetFps => { + if target_fps__.is_some() { + return Err(serde::de::Error::duplicate_field("targetFps")); + } + target_fps__ = + map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| x.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackSubscriptionOptions { + target_fps: target_fps__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSubscriptionOptions", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DeleteAgentDispatchRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -21065,6 +21731,9 @@ impl serde::Serialize for ParticipantInfo { if !self.kind_details.is_empty() { len += 1; } + if !self.data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.ParticipantInfo", len)?; if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; @@ -21128,6 +21797,9 @@ impl serde::Serialize for ParticipantInfo { }).collect::, _>>()?; struct_ser.serialize_field("kindDetails", &v)?; } + if !self.data_tracks.is_empty() { + struct_ser.serialize_field("dataTracks", &self.data_tracks)?; + } struct_ser.end() } } @@ -21159,6 +21831,8 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "disconnectReason", "kind_details", "kindDetails", + "data_tracks", + "dataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -21179,6 +21853,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { Attributes, DisconnectReason, KindDetails, + DataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -21217,6 +21892,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "attributes" => Ok(GeneratedField::Attributes), "disconnectReason" | "disconnect_reason" => Ok(GeneratedField::DisconnectReason), "kindDetails" | "kind_details" => Ok(GeneratedField::KindDetails), + "dataTracks" | "data_tracks" => Ok(GeneratedField::DataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -21252,6 +21928,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { let mut attributes__ = None; let mut disconnect_reason__ = None; let mut kind_details__ = None; + let mut data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Sid => { @@ -21358,6 +22035,12 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { } kind_details__ = Some(map_.next_value::>()?.into_iter().map(|x| x as i32).collect()); } + GeneratedField::DataTracks => { + if data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTracks")); + } + data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -21380,6 +22063,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { attributes: attributes__.unwrap_or_default(), disconnect_reason: disconnect_reason__.unwrap_or_default(), kind_details: kind_details__.unwrap_or_default(), + data_tracks: data_tracks__.unwrap_or_default(), }) } } @@ -22789,10 +23473,221 @@ impl<'de> serde::Deserialize<'de> for ProviderInfo { E: serde::de::Error, { match value { - "id" => Ok(GeneratedField::Id), - "name" => Ok(GeneratedField::Name), - "type" => Ok(GeneratedField::Type), - "preventTransfer" | "prevent_transfer" => Ok(GeneratedField::PreventTransfer), + "id" => Ok(GeneratedField::Id), + "name" => Ok(GeneratedField::Name), + "type" => Ok(GeneratedField::Type), + "preventTransfer" | "prevent_transfer" => Ok(GeneratedField::PreventTransfer), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ProviderInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut name__ = None; + let mut r#type__ = None; + let mut prevent_transfer__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Type => { + if r#type__.is_some() { + return Err(serde::de::Error::duplicate_field("type")); + } + r#type__ = Some(map_.next_value::()? as i32); + } + GeneratedField::PreventTransfer => { + if prevent_transfer__.is_some() { + return Err(serde::de::Error::duplicate_field("preventTransfer")); + } + prevent_transfer__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ProviderInfo { + id: id__.unwrap_or_default(), + name: name__.unwrap_or_default(), + r#type: r#type__.unwrap_or_default(), + prevent_transfer: prevent_transfer__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderType { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unknown => "PROVIDER_TYPE_UNKNOWN", + Self::Internal => "PROVIDER_TYPE_INTERNAL", + Self::External => "PROVIDER_TYPE_EXTERNAL", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for ProviderType { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "PROVIDER_TYPE_UNKNOWN", + "PROVIDER_TYPE_INTERNAL", + "PROVIDER_TYPE_EXTERNAL", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderType; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), + "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), + "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for ProxyConfig { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.url.is_empty() { + len += 1; + } + if !self.username.is_empty() { + len += 1; + } + if !self.password.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; + if !self.url.is_empty() { + struct_ser.serialize_field("url", &self.url)?; + } + if !self.username.is_empty() { + struct_ser.serialize_field("username", &self.username)?; + } + if !self.password.is_empty() { + struct_ser.serialize_field("password", &self.password)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProxyConfig { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "url", + "username", + "password", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Url, + Username, + Password, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "url" => Ok(GeneratedField::Url), + "username" => Ok(GeneratedField::Username), + "password" => Ok(GeneratedField::Password), _ => Ok(GeneratedField::__SkipField__), } } @@ -22802,137 +23697,189 @@ impl<'de> serde::Deserialize<'de> for ProviderInfo { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProviderInfo; + type Value = ProxyConfig; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ProviderInfo") + formatter.write_str("struct livekit.ProxyConfig") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut id__ = None; - let mut name__ = None; - let mut r#type__ = None; - let mut prevent_transfer__ = None; + let mut url__ = None; + let mut username__ = None; + let mut password__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Id => { - if id__.is_some() { - return Err(serde::de::Error::duplicate_field("id")); - } - id__ = Some(map_.next_value()?); - } - GeneratedField::Name => { - if name__.is_some() { - return Err(serde::de::Error::duplicate_field("name")); + GeneratedField::Url => { + if url__.is_some() { + return Err(serde::de::Error::duplicate_field("url")); } - name__ = Some(map_.next_value()?); + url__ = Some(map_.next_value()?); } - GeneratedField::Type => { - if r#type__.is_some() { - return Err(serde::de::Error::duplicate_field("type")); + GeneratedField::Username => { + if username__.is_some() { + return Err(serde::de::Error::duplicate_field("username")); } - r#type__ = Some(map_.next_value::()? as i32); + username__ = Some(map_.next_value()?); } - GeneratedField::PreventTransfer => { - if prevent_transfer__.is_some() { - return Err(serde::de::Error::duplicate_field("preventTransfer")); + GeneratedField::Password => { + if password__.is_some() { + return Err(serde::de::Error::duplicate_field("password")); } - prevent_transfer__ = Some(map_.next_value()?); + password__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ProviderInfo { - id: id__.unwrap_or_default(), - name: name__.unwrap_or_default(), - r#type: r#type__.unwrap_or_default(), - prevent_transfer: prevent_transfer__.unwrap_or_default(), + Ok(ProxyConfig { + url: url__.unwrap_or_default(), + username: username__.unwrap_or_default(), + password: password__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ProviderType { +impl serde::Serialize for PublishDataTrackRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - let variant = match self { - Self::Unknown => "PROVIDER_TYPE_UNKNOWN", - Self::Internal => "PROVIDER_TYPE_INTERNAL", - Self::External => "PROVIDER_TYPE_EXTERNAL", - }; - serializer.serialize_str(variant) + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pub_handle != 0 { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + if self.encryption != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; + } + struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ProviderType { +impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "PROVIDER_TYPE_UNKNOWN", - "PROVIDER_TYPE_INTERNAL", - "PROVIDER_TYPE_EXTERNAL", + "pub_handle", + "pubHandle", + "name", + "encryption", ]; - struct GeneratedVisitor; + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PubHandle, + Name, + Encryption, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProviderType; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "expected one of: {:?}", &FIELDS) - } + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } - fn visit_i64(self, v: i64) -> std::result::Result - where - E: serde::de::Error, - { - i32::try_from(v) - .ok() - .and_then(|x| x.try_into().ok()) - .ok_or_else(|| { - serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) - }) + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), + "name" => Ok(GeneratedField::Name), + "encryption" => Ok(GeneratedField::Encryption), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = PublishDataTrackRequest; - fn visit_u64(self, v: u64) -> std::result::Result - where - E: serde::de::Error, - { - i32::try_from(v) - .ok() - .and_then(|x| x.try_into().ok()) - .ok_or_else(|| { - serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) - }) + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.PublishDataTrackRequest") } - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, { - match value { - "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), - "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), - "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + let mut pub_handle__ = None; + let mut name__ = None; + let mut encryption__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); + } + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } + Ok(PublishDataTrackRequest { + pub_handle: pub_handle__.unwrap_or_default(), + name: name__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), + }) } } - deserializer.deserialize_any(GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ProxyConfig { +impl serde::Serialize for PublishDataTrackResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -22940,45 +23887,29 @@ impl serde::Serialize for ProxyConfig { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.url.is_empty() { - len += 1; - } - if !self.username.is_empty() { + if self.info.is_some() { len += 1; } - if !self.password.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; - if !self.url.is_empty() { - struct_ser.serialize_field("url", &self.url)?; - } - if !self.username.is_empty() { - struct_ser.serialize_field("username", &self.username)?; - } - if !self.password.is_empty() { - struct_ser.serialize_field("password", &self.password)?; + let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackResponse", len)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ProxyConfig { +impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "url", - "username", - "password", + "info", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Url, - Username, - Password, + Info, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -23001,9 +23932,7 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { E: serde::de::Error, { match value { - "url" => Ok(GeneratedField::Url), - "username" => Ok(GeneratedField::Username), - "password" => Ok(GeneratedField::Password), + "info" => Ok(GeneratedField::Info), _ => Ok(GeneratedField::__SkipField__), } } @@ -23013,52 +23942,36 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProxyConfig; + type Value = PublishDataTrackResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ProxyConfig") + formatter.write_str("struct livekit.PublishDataTrackResponse") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut url__ = None; - let mut username__ = None; - let mut password__ = None; + let mut info__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Url => { - if url__.is_some() { - return Err(serde::de::Error::duplicate_field("url")); - } - url__ = Some(map_.next_value()?); - } - GeneratedField::Username => { - if username__.is_some() { - return Err(serde::de::Error::duplicate_field("username")); - } - username__ = Some(map_.next_value()?); - } - GeneratedField::Password => { - if password__.is_some() { - return Err(serde::de::Error::duplicate_field("password")); + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); } - password__ = Some(map_.next_value()?); + info__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ProxyConfig { - url: url__.unwrap_or_default(), - username: username__.unwrap_or_default(), - password: password__.unwrap_or_default(), + Ok(PublishDataTrackResponse { + info: info__, }) } } - deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackResponse", FIELDS, GeneratedVisitor) } } impl serde::Serialize for RtcpSenderReportState { @@ -25837,6 +26750,12 @@ impl serde::Serialize for RequestResponse { request_response::Request::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + request_response::Request::PublishDataTrack(v) => { + struct_ser.serialize_field("publishDataTrack", v)?; + } + request_response::Request::UnpublishDataTrack(v) => { + struct_ser.serialize_field("unpublishDataTrack", v)?; + } } } struct_ser.end() @@ -25863,6 +26782,10 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "publish_data_track", + "publishDataTrack", + "unpublish_data_track", + "unpublishDataTrack", ]; #[allow(clippy::enum_variant_names)] @@ -25876,6 +26799,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { UpdateMetadata, UpdateAudioTrack, UpdateVideoTrack, + PublishDataTrack, + UnpublishDataTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -25907,6 +26832,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateMetadata" | "update_metadata" => Ok(GeneratedField::UpdateMetadata), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), + "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -25992,6 +26919,20 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateVideoTrack) +; + } + GeneratedField::PublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::PublishDataTrack) +; + } + GeneratedField::UnpublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UnpublishDataTrack) ; } GeneratedField::__SkipField__ => { @@ -26024,6 +26965,10 @@ impl serde::Serialize for request_response::Reason { Self::Queued => "QUEUED", Self::UnsupportedType => "UNSUPPORTED_TYPE", Self::UnclassifiedError => "UNCLASSIFIED_ERROR", + Self::InvalidHandle => "INVALID_HANDLE", + Self::InvalidName => "INVALID_NAME", + Self::DuplicateHandle => "DUPLICATE_HANDLE", + Self::DuplicateName => "DUPLICATE_NAME", }; serializer.serialize_str(variant) } @@ -26042,6 +26987,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED", "UNSUPPORTED_TYPE", "UNCLASSIFIED_ERROR", + "INVALID_HANDLE", + "INVALID_NAME", + "DUPLICATE_HANDLE", + "DUPLICATE_NAME", ]; struct GeneratedVisitor; @@ -26089,6 +27038,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED" => Ok(request_response::Reason::Queued), "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), + "INVALID_HANDLE" => Ok(request_response::Reason::InvalidHandle), + "INVALID_NAME" => Ok(request_response::Reason::InvalidName), + "DUPLICATE_HANDLE" => Ok(request_response::Reason::DuplicateHandle), + "DUPLICATE_NAME" => Ok(request_response::Reason::DuplicateName), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -35007,6 +35960,15 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + signal_request::Message::PublishDataTrackRequest(v) => { + struct_ser.serialize_field("publishDataTrackRequest", v)?; + } + signal_request::Message::UnpublishDataTrackRequest(v) => { + struct_ser.serialize_field("unpublishDataTrackRequest", v)?; + } + signal_request::Message::UpdateDataSubscription(v) => { + struct_ser.serialize_field("updateDataSubscription", v)?; + } } } struct_ser.end() @@ -35045,6 +36007,12 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "publish_data_track_request", + "publishDataTrackRequest", + "unpublish_data_track_request", + "unpublishDataTrackRequest", + "update_data_subscription", + "updateDataSubscription", ]; #[allow(clippy::enum_variant_names)] @@ -35066,6 +36034,9 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PingReq, UpdateAudioTrack, UpdateVideoTrack, + PublishDataTrackRequest, + UnpublishDataTrackRequest, + UpdateDataSubscription, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35105,6 +36076,9 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "pingReq" | "ping_req" => Ok(GeneratedField::PingReq), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "publishDataTrackRequest" | "publish_data_track_request" => Ok(GeneratedField::PublishDataTrackRequest), + "unpublishDataTrackRequest" | "unpublish_data_track_request" => Ok(GeneratedField::UnpublishDataTrackRequest), + "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), _ => Ok(GeneratedField::__SkipField__), } } @@ -35243,6 +36217,27 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateVideoTrack) +; + } + GeneratedField::PublishDataTrackRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrackRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::PublishDataTrackRequest) +; + } + GeneratedField::UnpublishDataTrackRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrackRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UnpublishDataTrackRequest) +; + } + GeneratedField::UpdateDataSubscription => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("updateDataSubscription")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateDataSubscription) ; } GeneratedField::__SkipField__ => { @@ -35349,6 +36344,15 @@ impl serde::Serialize for SignalResponse { signal_response::Message::SubscribedAudioCodecUpdate(v) => { struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; } + signal_response::Message::PublishDataTrackResponse(v) => { + struct_ser.serialize_field("publishDataTrackResponse", v)?; + } + signal_response::Message::UnpublishDataTrackResponse(v) => { + struct_ser.serialize_field("unpublishDataTrackResponse", v)?; + } + signal_response::Message::DataTrackSubscriberHandles(v) => { + struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; + } } } struct_ser.end() @@ -35402,6 +36406,12 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "mediaSectionsRequirement", "subscribed_audio_codec_update", "subscribedAudioCodecUpdate", + "publish_data_track_response", + "publishDataTrackResponse", + "unpublish_data_track_response", + "unpublishDataTrackResponse", + "data_track_subscriber_handles", + "dataTrackSubscriberHandles", ]; #[allow(clippy::enum_variant_names)] @@ -35431,6 +36441,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { RoomMoved, MediaSectionsRequirement, SubscribedAudioCodecUpdate, + PublishDataTrackResponse, + UnpublishDataTrackResponse, + DataTrackSubscriberHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35478,6 +36491,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), + "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), + "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), + "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -35671,6 +36687,27 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("subscribedAudioCodecUpdate")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) +; + } + GeneratedField::PublishDataTrackResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrackResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::PublishDataTrackResponse) +; + } + GeneratedField::UnpublishDataTrackResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrackResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::UnpublishDataTrackResponse) +; + } + GeneratedField::DataTrackSubscriberHandles => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) ; } GeneratedField::__SkipField__ => { @@ -38806,6 +39843,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { len += 1; } + if !self.publish_data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SyncState", len)?; if let Some(v) = self.answer.as_ref() { struct_ser.serialize_field("answer", v)?; @@ -38828,6 +39868,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { struct_ser.serialize_field("datachannelReceiveStates", &self.datachannel_receive_states)?; } + if !self.publish_data_tracks.is_empty() { + struct_ser.serialize_field("publishDataTracks", &self.publish_data_tracks)?; + } struct_ser.end() } } @@ -38849,6 +39892,8 @@ impl<'de> serde::Deserialize<'de> for SyncState { "trackSidsDisabled", "datachannel_receive_states", "datachannelReceiveStates", + "publish_data_tracks", + "publishDataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -38860,6 +39905,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { Offer, TrackSidsDisabled, DatachannelReceiveStates, + PublishDataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -38889,6 +39935,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { "offer" => Ok(GeneratedField::Offer), "trackSidsDisabled" | "track_sids_disabled" => Ok(GeneratedField::TrackSidsDisabled), "datachannelReceiveStates" | "datachannel_receive_states" => Ok(GeneratedField::DatachannelReceiveStates), + "publishDataTracks" | "publish_data_tracks" => Ok(GeneratedField::PublishDataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -38915,6 +39962,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { let mut offer__ = None; let mut track_sids_disabled__ = None; let mut datachannel_receive_states__ = None; + let mut publish_data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Answer => { @@ -38959,6 +40007,12 @@ impl<'de> serde::Deserialize<'de> for SyncState { } datachannel_receive_states__ = Some(map_.next_value()?); } + GeneratedField::PublishDataTracks => { + if publish_data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTracks")); + } + publish_data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -38972,6 +40026,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { offer: offer__, track_sids_disabled: track_sids_disabled__.unwrap_or_default(), datachannel_receive_states: datachannel_receive_states__.unwrap_or_default(), + publish_data_tracks: publish_data_tracks__.unwrap_or_default(), }) } } @@ -41522,6 +42577,424 @@ impl<'de> serde::Deserialize<'de> for TrickleRequest { deserializer.deserialize_struct("livekit.TrickleRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UnpublishDataTrackRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pub_handle != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackRequest", len)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pub_handle", + "pubHandle", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PubHandle, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnpublishDataTrackRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UnpublishDataTrackRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pub_handle__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); + } + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnpublishDataTrackRequest { + pub_handle: pub_handle__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UnpublishDataTrackRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for UnpublishDataTrackResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.info.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackResponse", len)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "info", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Info, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "info" => Ok(GeneratedField::Info), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnpublishDataTrackResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UnpublishDataTrackResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut info__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); + } + info__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnpublishDataTrackResponse { + info: info__, + }) + } + } + deserializer.deserialize_struct("livekit.UnpublishDataTrackResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for UpdateDataSubscription { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.updates.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription", len)?; + if !self.updates.is_empty() { + struct_ser.serialize_field("updates", &self.updates)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "updates", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Updates, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "updates" => Ok(GeneratedField::Updates), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UpdateDataSubscription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UpdateDataSubscription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut updates__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Updates => { + if updates__.is_some() { + return Err(serde::de::Error::duplicate_field("updates")); + } + updates__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UpdateDataSubscription { + updates: updates__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UpdateDataSubscription", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for update_data_subscription::Update { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.track_sid.is_empty() { + len += 1; + } + if self.subscribe { + len += 1; + } + if self.options.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription.Update", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + if self.subscribe { + struct_ser.serialize_field("subscribe", &self.subscribe)?; + } + if let Some(v) = self.options.as_ref() { + struct_ser.serialize_field("options", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "track_sid", + "trackSid", + "subscribe", + "options", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TrackSid, + Subscribe, + Options, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "subscribe" => Ok(GeneratedField::Subscribe), + "options" => Ok(GeneratedField::Options), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = update_data_subscription::Update; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UpdateDataSubscription.Update") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut track_sid__ = None; + let mut subscribe__ = None; + let mut options__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::Subscribe => { + if subscribe__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribe")); + } + subscribe__ = Some(map_.next_value()?); + } + GeneratedField::Options => { + if options__.is_some() { + return Err(serde::de::Error::duplicate_field("options")); + } + options__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(update_data_subscription::Update { + track_sid: track_sid__.unwrap_or_default(), + subscribe: subscribe__.unwrap_or_default(), + options: options__, + }) + } + } + deserializer.deserialize_struct("livekit.UpdateDataSubscription.Update", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for UpdateIngressRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index 87f415914..cd0db3386 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -30,6 +30,7 @@ livekit-runtime = { workspace = true } livekit-api = { workspace = true } libwebrtc = { workspace = true } livekit-protocol = { workspace = true } +livekit-datatrack = { workspace = true } prost = "0.12" serde = { version = "1", features = ["derive"] } serde_json = "1.0" @@ -48,3 +49,4 @@ bmrng = "0.5.2" [dev-dependencies] anyhow = "1.0.99" test-log = "0.2.18" +test-case = "3.3" \ No newline at end of file diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 3e6bda23b..1f230e9e3 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -15,6 +15,10 @@ pub use livekit_protocol::AudioTrackFeature; pub use crate::{ + data_track::{ + DataTrackFrame, DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError, + PushFrameError, PushFrameErrorReason, RemoteDataTrack, + }, id::*, participant::{ ConnectionQuality, DisconnectReason, LocalParticipant, Participant, PerformRpcData, diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index 20ca31e03..65575a4e7 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -155,6 +155,7 @@ impl From for participant::ParticipantKind { participant_info::Kind::Sip => participant::ParticipantKind::Sip, participant_info::Kind::Agent => participant::ParticipantKind::Agent, participant_info::Kind::Connector => participant::ParticipantKind::Connector, + participant_info::Kind::Bridge => participant::ParticipantKind::Bridge } } } @@ -174,6 +175,9 @@ impl From for participant::ParticipantKindDetail { participant_info::KindDetail::ConnectorTwilio => { participant::ParticipantKindDetail::ConnectorTwilio } + participant_info::KindDetail::BridgeRtsp => { + participant::ParticipantKindDetail::BridgeRtsp + } } } } diff --git a/livekit/src/room/data_track.rs b/livekit/src/room/data_track.rs new file mode 100644 index 000000000..52e83917b --- /dev/null +++ b/livekit/src/room/data_track.rs @@ -0,0 +1,16 @@ +// 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. + +// Re-export everything in the "api" module publicly. +pub use livekit_datatrack::api::*; diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs new file mode 100644 index 000000000..7f1bbf877 --- /dev/null +++ b/livekit/src/room/e2ee/data_track.rs @@ -0,0 +1,81 @@ +// 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::{id::ParticipantIdentity, E2eeManager}; +use bytes::Bytes; +use livekit_datatrack::backend as dt; + +/// Wrapper around [`E2eeManager`] implementing [`dt::EncryptionProvider`]. +#[derive(Debug)] +pub(crate) struct DataTrackEncryptionProvider { + manager: E2eeManager, + sender_identity: ParticipantIdentity, +} + +impl DataTrackEncryptionProvider { + pub fn new(manager: E2eeManager, sender_identity: ParticipantIdentity) -> Self { + Self { manager, sender_identity } + } +} + +impl dt::EncryptionProvider for DataTrackEncryptionProvider { + fn encrypt(&self, payload: bytes::Bytes) -> Result { + let key_index = self + .manager + .key_provider() + .map_or(0, |kp| kp.get_latest_key_index() as u32); + + let encrypted = self + .manager + .encrypt_data(payload.into(), &self.sender_identity, key_index) + .map_err(|_| dt::EncryptionError)?; + + let payload = encrypted.data.into(); + let iv = encrypted.iv.try_into().map_err(|_| dt::EncryptionError)?; + let key_index = encrypted.key_index.try_into().map_err(|_| dt::EncryptionError)?; + + Ok(dt::EncryptedPayload { payload, iv, key_index }) + } +} + +/// Wrapper around [`E2eeManager`] implementing [`dt::DecryptionProvider`]. +#[derive(Debug)] +pub(crate) struct DataTrackDecryptionProvider { + manager: E2eeManager, +} + +impl DataTrackDecryptionProvider { + pub fn new(manager: E2eeManager) -> Self { + Self { manager } + } +} + +impl dt::DecryptionProvider for DataTrackDecryptionProvider { + fn decrypt( + &self, + payload: dt::EncryptedPayload, + sender_identity: &str, + ) -> Result { + let decrypted = self + .manager + .decrypt_data( + payload.payload.into(), + payload.iv.to_vec(), + payload.key_index as u32, + sender_identity, + ) + .ok_or_else(|| dt::DecryptionError)?; + Ok(Bytes::from(decrypted)) + } +} diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 1e583b9c4..47ade5c75 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -31,6 +31,7 @@ use crate::{ prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, rtc_engine::lk_runtime::LkRuntime, }; +use std::fmt::Debug; type StateChangedHandler = Box; @@ -246,19 +247,18 @@ impl E2eeManager { } /// Decrypt data received from a data channel - pub fn handle_encrypted_data( + pub(crate) fn decrypt_data( &self, - data: &[u8], - iv: &[u8], - participant_identity: &str, + data: Vec, + iv: Vec, key_index: u32, + participant_identity: &str, ) -> Option> { let inner = self.inner.lock(); let data_packet_cryptor = inner.data_packet_cryptor.as_ref()?; - let encrypted_packet = EncryptedPacket { data: data.to_vec(), iv: iv.to_vec(), key_index }; - + let encrypted_packet = EncryptedPacket { data, iv, key_index }; match data_packet_cryptor.decrypt(participant_identity, &encrypted_packet) { Ok(decrypted_data) => Some(decrypted_data), Err(e) => { @@ -269,17 +269,25 @@ impl E2eeManager { } /// Encrypt data for transmission over a data channel - pub fn encrypt_data( + pub(crate) fn encrypt_data( &self, - data: &[u8], - participant_identity: &str, - key_index: u32, + data: Vec, + participant_identity: &ParticipantIdentity, + key_index: u32 ) -> Result> { let inner = self.inner.lock(); let data_packet_cryptor = inner.data_packet_cryptor.as_ref().ok_or("DataPacketCryptor is not initialized")?; - data_packet_cryptor.encrypt(participant_identity, key_index, data) + data_packet_cryptor.encrypt(participant_identity.as_str(), key_index, &data) + } +} + +impl Debug for E2eeManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("E2eeManager") + .field("enabled", &self.inner.lock().enabled) + .finish_non_exhaustive() } } diff --git a/livekit/src/room/e2ee/mod.rs b/livekit/src/room/e2ee/mod.rs index 8864dc421..e1235d81d 100644 --- a/livekit/src/room/e2ee/mod.rs +++ b/livekit/src/room/e2ee/mod.rs @@ -19,6 +19,9 @@ use self::key_provider::KeyProvider; pub mod key_provider; pub mod manager; +/// Provider implementations for data track. +pub(crate) mod data_track; + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum EncryptionType { #[default] diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index ff5276d6d..8e9b15369 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. use bmrng::unbounded::UnboundedRequestReceiver; +use futures_util::{Stream, StreamExt}; use libwebrtc::{ native::frame_cryptor::EncryptionState, prelude::{ @@ -23,6 +24,7 @@ use libwebrtc::{ RtcError, }; use livekit_api::signal_client::{SignalOptions, SignalSdkOptions}; +use livekit_datatrack::{api::RemoteDataTrack, backend as dt}; use livekit_protocol::observer::Dispatcher; use livekit_protocol::{self as proto, encryption}; use livekit_runtime::JoinHandle; @@ -45,6 +47,7 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ + e2ee::data_track::{DataTrackDecryptionProvider, DataTrackEncryptionProvider}, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, @@ -55,6 +58,7 @@ use crate::{ }; pub mod data_stream; +pub mod data_track; pub mod e2ee; pub mod id; pub mod options; @@ -241,6 +245,8 @@ pub enum RoomEvent { TokenRefreshed { token: String, }, + /// A remote participant published a data track. + RemoteDataTrackPublished(RemoteDataTrack), } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -446,6 +452,8 @@ pub(crate) struct RoomSession { e2ee_manager: E2eeManager, incoming_stream_manager: IncomingStreamManager, outgoing_stream_manager: OutgoingStreamManager, + local_dt_input: dt::local::ManagerInput, + remote_dt_input: dt::remote::ManagerInput, handle: AsyncMutex>, } @@ -453,6 +461,10 @@ struct Handle { room_handle: JoinHandle<()>, incoming_stream_handle: JoinHandle<()>, outgoing_stream_handle: JoinHandle<()>, + local_dt_task: JoinHandle<()>, + local_dt_forward_task: JoinHandle<()>, + remote_dt_task: JoinHandle<()>, + remote_dt_forward_task: JoinHandle<()>, close_tx: broadcast::Sender<()>, } @@ -474,13 +486,16 @@ impl Room { mut options: RoomOptions, ) -> RoomResult<(Self, mpsc::UnboundedReceiver)> { // TODO(theomonnom): move connection logic to the RoomSession + let with_dc_encryption = options.encryption.is_some(); let encryption_options = options.encryption.take().or(options.e2ee.take()); let e2ee_manager = E2eeManager::new(encryption_options, with_dc_encryption); + let mut signal_options = SignalOptions::default(); signal_options.sdk_options = options.sdk_options.clone().into(); signal_options.auto_subscribe = options.auto_subscribe; signal_options.adaptive_stream = options.adaptive_stream; + let (rtc_engine, join_response, engine_events) = RtcEngine::connect( url, token, @@ -593,6 +608,25 @@ impl Room { } }); + let decryption_provider = e2ee_manager.enabled().then(|| { + Arc::new(DataTrackEncryptionProvider::new( + e2ee_manager.clone(), + local_participant.identity().clone(), + )) as Arc + }); + let encryption_provider = e2ee_manager.enabled().then(|| { + Arc::new(DataTrackDecryptionProvider::new(e2ee_manager.clone())) + as Arc + }); + + let local_dt_options = dt::local::ManagerOptions { decryption_provider }; + let (local_dt_manager, local_dt_input, local_dt_output) = + dt::local::Manager::new(local_dt_options); + + let remote_dt_options = dt::remote::ManagerOptions { encryption_provider }; + let (remote_dt_manager, remote_dt_input, remote_dt_output) = + dt::remote::Manager::new(remote_dt_options); + let (incoming_stream_manager, open_rx) = IncomingStreamManager::new(); let (outgoing_stream_manager, packet_rx) = OutgoingStreamManager::new(); @@ -623,6 +657,8 @@ impl Room { e2ee_manager: e2ee_manager.clone(), incoming_stream_manager, outgoing_stream_manager, + local_dt_input, + remote_dt_input, handle: Default::default(), }); inner.local_participant.set_session(Arc::downgrade(&inner)); @@ -694,10 +730,28 @@ impl Room { close_rx.resubscribe(), )); + let local_dt_task = livekit_runtime::spawn(local_dt_manager.run()); + let local_dt_forward_task = livekit_runtime::spawn( + inner.clone().local_dt_forward_task(local_dt_output, close_rx.resubscribe()), + ); + + let remote_dt_task = livekit_runtime::spawn(remote_dt_manager.run()); + let remote_dt_forward_task = livekit_runtime::spawn( + inner.clone().remote_dt_forward_task(remote_dt_output, close_rx.resubscribe()), + ); + let room_handle = livekit_runtime::spawn(inner.clone().room_task(engine_events, close_rx)); - let handle = - Handle { room_handle, incoming_stream_handle, outgoing_stream_handle, close_tx }; + let handle = Handle { + room_handle, + incoming_stream_handle, + outgoing_stream_handle, + local_dt_task, + local_dt_forward_task, + remote_dt_task, + remote_dt_forward_task, + close_tx, + }; inner.handle.lock().await.replace(handle); Ok((Self { inner }, events)) @@ -932,7 +986,12 @@ impl RoomSession { EngineEvent::TrackMuted { sid, muted } => { self.handle_server_initiated_mute_track(sid, muted); } - _ => {} + EngineEvent::LocalDataTrackInput(event) => { + _ = self.local_dt_input.send(event); + } + EngineEvent::RemoteDataTrackInput(event) => { + _ = self.remote_dt_input.send(event); + } } Ok(()) @@ -952,6 +1011,10 @@ impl RoomSession { let _ = handle.close_tx.send(()); let _ = handle.incoming_stream_handle.await; let _ = handle.outgoing_stream_handle.await; + let _ = handle.local_dt_forward_task.await; + let _ = handle.local_dt_task.await; + let _ = handle.remote_dt_forward_task.await; + let _ = handle.remote_dt_task.await; let _ = handle.room_handle.await; self.dispatcher.clear(); @@ -1205,6 +1268,10 @@ impl RoomSession { }); } + let publish_data_tracks = dt::local::publish_responses_for_sync_state( + self.local_dt_input.query_tracks().await, + ); + let sync_state = proto::SyncState { answer: Some(proto::SessionDescription { sdp: answer.to_string(), @@ -1227,6 +1294,7 @@ impl RoomSession { publish_tracks: self.local_participant.published_tracks_info(), data_channels: dcs, datachannel_receive_states: session.data_channel_receive_states(), + publish_data_tracks, }; log::debug!("sending sync state {:?}", sync_state); @@ -1355,6 +1423,9 @@ impl RoomSession { fn handle_restarted(self: &Arc, tx: oneshot::Sender<()>) { let _ = tx.send(()); + // Ensure SFU continues delivering packets for existing data track subscriptions. + _ = self.remote_dt_input.send(dt::remote::InputEvent::ResendSubscriptionUpdates); + // Unpublish and republish every track // At this time we know that the RtcSession is successfully restarted let published_tracks = self.local_participant.track_publications(); @@ -1804,6 +1875,51 @@ impl RoomSession { let event = RoomEvent::TokenRefreshed { token }; self.dispatcher.dispatch(&event); } + + /// Task for handling output events from the local data track manager. + async fn local_dt_forward_task( + self: Arc, + mut events: impl Stream + Unpin, + mut close_rx: broadcast::Receiver<()>, + ) { + loop { + tokio::select! { + event = events.next() => match event { + Some(event) => _ = self.rtc_engine.handle_local_data_track_output(event).await, + None => break, + }, + _ = close_rx.recv() => { + _ = self.local_dt_input.send(dt::local::InputEvent::Shutdown); + break; + }, + } + } + } + + /// Task for handling output events from the remote data track manager. + async fn remote_dt_forward_task( + self: Arc, + mut events: impl Stream + Unpin, + mut close_rx: broadcast::Receiver<()>, + ) { + loop { + tokio::select! { + event = events.next() => match event { + Some(event) => match event { + dt::remote::OutputEvent::TrackAvailable(track) => { + _ = self.dispatcher.dispatch(&RoomEvent::RemoteDataTrackPublished(track)); + } + other => _ = self.rtc_engine.handle_remote_data_track_output(other).await + }, + None => break, + }, + _ = close_rx.recv() => { + _ = self.remote_dt_input.send(dt::remote::InputEvent::Shutdown); + break; + }, + } + } + } } /// Receives stream readers for newly-opened streams and dispatches room events. diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index b8d101225..bf4734b09 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -31,6 +31,7 @@ use crate::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, }, + data_track::{self, DataTrack, DataTrackOptions, Local}, e2ee::EncryptionType, options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, prelude::*, @@ -249,6 +250,40 @@ impl LocalParticipant { vec } + /// Publishes a data track. + /// + /// # Returns + /// + /// The published data track if successful. Use [`LocalDataTrack::publish`] + /// to send data frames on the track. + /// + /// # Examples + /// + /// Publish a track named "my_track": + /// + /// ``` + /// # use livekit::prelude::*; + /// # async fn with_room(room: Room) -> Result<(), PublishError> { + /// let track = room + /// .local_participant() + /// .publish_data_track("my_track") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// + pub async fn publish_data_track( + &self, + options: impl Into, + ) -> Result, data_track::PublishError> { + self.session() + .ok_or(PublishError::Disconnected)? + .local_dt_input + .publish_track(options.into()) + .await + } + + /// Publishes a media track. pub async fn publish_track( &self, track: LocalTrack, @@ -505,6 +540,11 @@ impl LocalParticipant { self.inner.rtc_engine.publish_data(packet, kind, true).await.map_err(Into::into) } + /// Publishes a data packet. + /// + /// This API will be deprecated in the near future. For new applications, + /// please consider using data tracks (see [`LocalParticipant::publish_data_track`]). + /// pub async fn publish_data(&self, packet: DataPacket) -> RoomResult<()> { let kind = match packet.reliable { true => DataPacketKind::Reliable, diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index f4006644b..4fabfb89d 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -45,6 +45,7 @@ pub enum ParticipantKind { Sip, Agent, Connector, + Bridge } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -53,6 +54,7 @@ pub enum ParticipantKindDetail { Forwarded, ConnectorWhatsapp, ConnectorTwilio, + BridgeRtsp } #[derive(Debug, Clone, Copy, Eq, PartialEq)] diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index c14f8d0a6..b1c3d2b4d 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; - use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; +use livekit_datatrack::backend as dt; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; +use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::{ mpsc, oneshot, Mutex as AsyncMutex, Notify, RwLock as AsyncRwLock, @@ -189,6 +189,8 @@ pub enum EngineEvent { sid: String, muted: bool, }, + LocalDataTrackInput(dt::local::InputEvent), + RemoteDataTrackInput(dt::remote::InputEvent), } /// Represents a running RtcSession with the ability to close the session @@ -271,6 +273,24 @@ impl RtcEngine { session.simulate_scenario(scenario).await } + pub async fn handle_local_data_track_output(&self, event: dt::local::OutputEvent) -> EngineResult<()> { + let (session, _r_lock) = { + let (handle, _r_lock) = self.inner.wait_reconnection().await?; + (handle.session.clone(), _r_lock) + }; + session.handle_local_data_track_output(event).await; + Ok(()) + } + + pub async fn handle_remote_data_track_output(&self, event: dt::remote::OutputEvent) -> EngineResult<()> { + let (session, _r_lock) = { + let (handle, _r_lock) = self.inner.wait_reconnection().await?; + (handle.session.clone(), _r_lock) + }; + session.handle_remote_data_track_output(event).await; + Ok(()) + } + pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; @@ -609,6 +629,12 @@ impl EngineInner { SessionEvent::TrackMuted { sid, muted } => { let _ = self.engine_tx.send(EngineEvent::TrackMuted { sid, muted }); } + SessionEvent::LocalDataTrackInput(event) => { + let _ = self.engine_tx.send(EngineEvent::LocalDataTrackInput(event)); + } + SessionEvent::RemoteDataTrackInput(event) => { + let _ = self.engine_tx.send(EngineEvent::RemoteDataTrackInput(event)); + } } Ok(()) } @@ -857,3 +883,9 @@ impl EngineInner { session.wait_pc_connection().await } } + +impl From for EngineError { + fn from(err: livekit_datatrack::api::InternalError) -> Self { + Self::Internal(err.to_string().into()) + } +} diff --git a/livekit/src/rtc_engine/rtc_events.rs b/livekit/src/rtc_engine/rtc_events.rs index d5f59f794..a59aab58f 100644 --- a/livekit/src/rtc_engine/rtc_events.rs +++ b/livekit/src/rtc_engine/rtc_events.rs @@ -18,7 +18,10 @@ use tokio::sync::mpsc; use super::peer_transport::PeerTransport; use crate::{ - rtc_engine::{peer_transport::OnOfferCreated, rtc_session::RELIABLE_DC_LABEL}, + rtc_engine::{ + peer_transport::OnOfferCreated, + rtc_session::{LOSSY_DC_LABEL, RELIABLE_DC_LABEL}, + }, DataPacketKind, }; @@ -94,13 +97,15 @@ fn on_data_channel( emitter: RtcEmitter, ) -> rtc::peer_connection::OnDataChannel { Box::new(move |data_channel| { - let kind = if data_channel.label() == RELIABLE_DC_LABEL { - DataPacketKind::Reliable - } else { - DataPacketKind::Lossy - }; - data_channel.on_message(Some(on_message(emitter.clone(), kind))); - + match data_channel.label().as_str() { + RELIABLE_DC_LABEL => { + data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Reliable))) + } + LOSSY_DC_LABEL => { + data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Lossy))) + } + _ => {} + } let _ = emitter.send(RtcEvent::DataChannel { data_channel, target }); }) } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index f111fdbf4..dc5797677 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,8 +23,10 @@ use std::{ time::Duration, }; +use bytes::Bytes; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; +use livekit_datatrack::backend as dt; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; @@ -34,7 +36,10 @@ use proto::{ SignalTarget, }; use serde::{Deserialize, Serialize}; -use tokio::sync::{mpsc, oneshot, watch, Notify}; +use tokio::sync::{ + mpsc::{self, WeakUnboundedSender}, + oneshot, watch, Notify, +}; use super::{rtc_events, EngineError, EngineOptions, EngineResult, SimulateScenario}; use crate::{ @@ -63,6 +68,7 @@ pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub const LOSSY_DC_LABEL: &str = "_lossy"; pub const RELIABLE_DC_LABEL: &str = "_reliable"; +pub const DATA_TRACK_DC_LABEL: &str = "_data_track"; pub const RELIABLE_RECEIVED_STATE_TTL: Duration = Duration::from_secs(30); pub const PUBLISHER_NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150); pub const INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD: u64 = 2 * 1024 * 1024; @@ -198,6 +204,8 @@ pub enum SessionEvent { sid: String, muted: bool, }, + LocalDataTrackInput(dt::local::InputEvent), + RemoteDataTrackInput(dt::remote::InputEvent), } #[derive(Debug)] @@ -345,6 +353,7 @@ struct SessionInner { lossy_dc_buffered_amount_low_threshold: AtomicU64, reliable_dc: DataChannel, reliable_dc_buffered_amount_low_threshold: AtomicU64, + dt_transport: DataChannel, /// Next sequence number for reliable packets. next_packet_sequence: AtomicU32, @@ -360,6 +369,7 @@ struct SessionInner { // so we can receive data from other participants sub_lossy_dc: Mutex>, sub_reliable_dc: Mutex>, + sub_dt_transport: Mutex>, closed: AtomicBool, emitter: SessionEmitter, @@ -422,7 +432,7 @@ impl RtcSession { ) -> EngineResult<(Self, proto::JoinResponse, SessionEvents)> { let (emitter, session_events) = mpsc::unbounded_channel(); - let (signal_client, join_response, signal_events) = + let (signal_client, mut join_response, signal_events) = SignalClient::connect(url, token, options.signal_options.clone()).await?; let signal_client = Arc::new(signal_client); log::debug!("received JoinResponse: {:?}", join_response); @@ -430,6 +440,9 @@ impl RtcSession { let Some(participant_info) = SessionParticipantInfo::from_join(&join_response) else { Err(EngineError::Internal("Join response missing participant info".into()))? }; + if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { + _ = emitter.send(SessionEvent::RemoteDataTrackInput(initial_publications.into())); + } let (rtc_emitter, rtc_events) = mpsc::unbounded_channel(); let rtc_config = make_rtc_config_join(join_response.clone(), options.rtc_config.clone()); @@ -447,20 +460,22 @@ impl RtcSession { proto::SignalTarget::Subscriber, ); - let mut lossy_dc = publisher_pc.peer_connection().create_data_channel( - LOSSY_DC_LABEL, - DataChannelInit { - ordered: false, - max_retransmits: Some(0), - ..DataChannelInit::default() - }, - )?; - let mut reliable_dc = publisher_pc.peer_connection().create_data_channel( RELIABLE_DC_LABEL, - DataChannelInit { ordered: true, ..DataChannelInit::default() }, + DataChannelInit { ordered: true, ..Default::default() }, )?; + let lossy_options = + DataChannelInit { ordered: false, max_retransmits: Some(0), ..Default::default() }; + + let mut lossy_dc = publisher_pc + .peer_connection() + .create_data_channel(LOSSY_DC_LABEL, lossy_options.clone())?; + + let dt_transport = publisher_pc + .peer_connection() + .create_data_channel(DATA_TRACK_DC_LABEL, lossy_options)?; + // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); rtc_events::forward_pc_events(&mut subscriber_pc, rtc_emitter.clone()); @@ -484,12 +499,14 @@ impl RtcSession { reliable_dc_buffered_amount_low_threshold: AtomicU64::new( INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD, ), + dt_transport, next_packet_sequence: 1.into(), packet_rx_state: Mutex::new(TtlMap::new(RELIABLE_RECEIVED_STATE_TTL)), participant_info, dc_emitter, sub_lossy_dc: Mutex::new(None), sub_reliable_dc: Mutex::new(None), + sub_dt_transport: Mutex::new(None), closed: Default::default(), emitter, options, @@ -504,7 +521,8 @@ impl RtcSession { livekit_runtime::spawn(inner.clone().signal_task(signal_events, close_rx.clone())); let rtc_task = livekit_runtime::spawn(inner.clone().rtc_session_task(rtc_events, close_rx.clone())); - let dc_task = livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx)); + let dc_task = + livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); let handle = Mutex::new(Some(SessionHandle { close_tx, signal_task, rtc_task, dc_task })); @@ -605,6 +623,55 @@ impl RtcSession { self.inner.data_channel(target, kind) } + /// Handles an event from the local data track manager. + pub(super) async fn handle_local_data_track_output(&self, event: dt::local::OutputEvent) { + use dt::local::OutputEvent; + match event.into() { + OutputEvent::SfuPublishRequest(event) => { + if let Err(err) = self.inner.ensure_data_track_publisher_connected().await { + log::error!("Failed to open data track publish transport: {}", err); + } + self.signal_client() + .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) + .await + } + OutputEvent::SfuUnpublishRequest(event) => { + self.signal_client() + .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) + .await + } + OutputEvent::PacketsAvailable(packets) => self.try_send_data_track_packets(packets), + } + } + + /// Handles an event from the remote data track manager. + pub(super) async fn handle_remote_data_track_output(&self, event: dt::remote::OutputEvent) { + use dt::remote::OutputEvent; + match event.into() { + OutputEvent::SfuUpdateSubscription(event) => { + self.signal_client() + .send(proto::signal_request::Message::UpdateDataSubscription(event.into())) + .await + } + _ => {} + } + } + + /// Try to send data track packets over the transport. + /// + /// Packets will be sent until the first error, at which point the rest of + /// the batch will be dropped. + /// + fn try_send_data_track_packets(&self, packets: Vec) { + // TODO: handle buffering + for packet in packets { + if let Err(err) = self.inner.dt_transport.send(&packet, true) { + log::trace!("Failed to send packet: {}", err); + break; + } + } + } + pub fn e2ee_manager(&self) -> Option { self.inner.e2ee_manager.clone() } @@ -966,7 +1033,14 @@ impl SessionInner { true, ); } - proto::signal_response::Message::Update(update) => { + proto::signal_response::Message::Update(mut update) => { + let local_participant_identity = self.participant_info.identity.as_str().into(); + if let Ok(event) = dt::remote::event_from_participant_update( + &mut update, + local_participant_identity, + ) { + _ = self.emitter.send(SessionEvent::RemoteDataTrackInput(event.into())); + } let _ = self .emitter .send(SessionEvent::ParticipantUpdate { updates: update.participants }); @@ -998,11 +1072,29 @@ impl SessionInner { }); } proto::signal_response::Message::RequestResponse(request_response) => { + if let Some(event) = + dt::local::publish_result_from_request_response(&request_response) + { + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); + return Ok(()); + } let mut pending_requests = self.pending_requests.lock(); if let Some(tx) = pending_requests.remove(&request_response.request_id) { let _ = tx.send(request_response); } } + proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { + let event: dt::local::SfuPublishResponse = publish_res.try_into()?; + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); + } + proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { + let event: dt::local::SfuUnpublishResponse = unpublish_res.try_into()?; + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); + } + proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { + let event: dt::remote::SfuSubscriberHandles = subscriber_handles.try_into()?; + _ = self.emitter.send(SessionEvent::RemoteDataTrackInput(event.into())); + } proto::signal_response::Message::RefreshToken(ref token) => { let url = self.signal_client.url(); let _ = self.emitter.send(SessionEvent::RefreshToken { url, token: token.clone() }); @@ -1049,13 +1141,19 @@ impl SessionInner { } RtcEvent::DataChannel { data_channel, target } => { log::debug!("received data channel: {:?} {:?}", data_channel, target); - if target == SignalTarget::Subscriber { - if data_channel.label() == LOSSY_DC_LABEL { - self.sub_lossy_dc.lock().replace(data_channel); - } else if data_channel.label() == RELIABLE_DC_LABEL { - self.sub_reliable_dc.lock().replace(data_channel); - } + if target != SignalTarget::Subscriber { + return Ok(()); } + let dc_ref = match data_channel.label().as_str() { + LOSSY_DC_LABEL => &self.sub_lossy_dc, + RELIABLE_DC_LABEL => &self.sub_reliable_dc, + DATA_TRACK_DC_LABEL => { + handle_remote_dt_packets(&data_channel, self.emitter.downgrade()); + &self.sub_dt_transport + } + _ => return Ok(()), + }; + dc_ref.lock().replace(data_channel); } RtcEvent::Offer { offer, target: _ } => { // Send the publisher offer to the server @@ -1229,14 +1327,15 @@ impl SessionInner { proto::data_packet::Value::EncryptedPacket(encrypted_packet) => { // Handle encrypted data packets if let Some(e2ee_manager) = &self.e2ee_manager { + let encryption_type = encrypted_packet.encryption_type(); let participant_identity_str = participant_identity.as_ref().map(|p| p.0.as_str()).unwrap_or(""); - match e2ee_manager.handle_encrypted_data( - &encrypted_packet.encrypted_value, - &encrypted_packet.iv, - participant_identity_str, + match e2ee_manager.decrypt_data( + encrypted_packet.encrypted_value, + encrypted_packet.iv, encrypted_packet.key_index, + participant_identity_str, ) { Some(decrypted_payload) => { // Parse the decrypted payload as EncryptedPacketPayload @@ -1249,7 +1348,7 @@ impl SessionInner { participant_sid, participant_identity, convert_encrypted_to_data_packet_value(decrypted_value), - encrypted_packet.encryption_type(), + encryption_type, ); Ok(()) } else { @@ -1522,13 +1621,14 @@ impl SessionInner { // Encode the payload and encrypt it let payload_bytes = encrypted_payload.encode_to_vec(); + let key_index = e2ee_manager .key_provider() - .map(|kp| kp.get_latest_key_index() as u32) - .unwrap_or(0); + .map_or(0, |kp| kp.get_latest_key_index() as u32); + match e2ee_manager.encrypt_data( - &payload_bytes, - &self.participant_info.identity.0, + payload_bytes, + &self.participant_info.identity, key_index, ) { Ok(encrypted_data) => { @@ -1745,11 +1845,29 @@ impl SessionInner { } } - /// Ensure the Publisher PC is connected, if not, start the negotiation - /// This is required when sending data to the server + /// Ensure the publisher peer connection and data channel for the specified packet + /// type are connected. If not, start the negotiation. + /// + /// This is required when sending data to the server. + /// async fn ensure_publisher_connected( self: &Arc, kind: DataPacketKind, + ) -> EngineResult<()> { + let required_dc = self.data_channel(SignalTarget::Publisher, kind).unwrap(); + self.ensure_publisher_connected_with_dc(required_dc).await?; + Ok(()) + } + + /// Ensure the required data channel for publishing data track frames is open. + async fn ensure_data_track_publisher_connected(self: &Arc) -> EngineResult<()> { + self.ensure_publisher_connected_with_dc(self.dt_transport.clone()).await?; + Ok(()) + } + + async fn ensure_publisher_connected_with_dc( + self: &Arc, + required_dc: DataChannel, ) -> EngineResult<()> { if !self.has_published.load(Ordering::Acquire) { // The publisher has never been connected, start the negotiation @@ -1757,14 +1875,14 @@ impl SessionInner { self.publisher_negotiation_needed(); } - let dc = self.data_channel(SignalTarget::Publisher, kind).unwrap(); - if dc.state() == DataChannelState::Open { + if required_dc.state() == DataChannelState::Open { return Ok(()); } // Wait until the PeerConnection is connected let wait_connected = async { - while !self.publisher_pc.is_connected() || dc.state() != DataChannelState::Open { + while !self.publisher_pc.is_connected() || required_dc.state() != DataChannelState::Open + { if self.closed.load(Ordering::Acquire) { return Err(EngineError::Connection("closed".into())); } @@ -1821,6 +1939,20 @@ impl SessionInner { } } +/// Emit incoming data track packets as session events. +pub fn handle_remote_dt_packets(dc: &DataChannel, emitter: WeakUnboundedSender) { + let on_message: libwebrtc::data_channel::OnMessage = Box::new(move |buffer: DataBuffer| { + if !buffer.binary { + log::error!("Received non-binary message"); + return; + } + let packet: Bytes = buffer.data.to_vec().into(); // TODO: avoid clone if possible + let Some(emitter) = emitter.upgrade() else { return }; + _ = emitter.send(SessionEvent::RemoteDataTrackInput(packet.into())); + }); + dc.on_message(on_message.into()); +} + macro_rules! make_rtc_config { ($fncname:ident, $proto:ty) => { fn $fncname(value: $proto, mut config: RtcConfiguration) -> RtcConfiguration { diff --git a/livekit/tests/common/e2e/mod.rs b/livekit/tests/common/e2e/mod.rs index 44c27010c..6ff1c84d8 100644 --- a/livekit/tests/common/e2e/mod.rs +++ b/livekit/tests/common/e2e/mod.rs @@ -44,14 +44,43 @@ impl TestEnvironment { } } +#[derive(Debug, Clone)] +pub struct TestRoomOptions { + /// Grants for the generated token. + pub grants: VideoGrants, + /// Options used for creating the [`Room`]. + pub room: RoomOptions, +} + +impl Default for TestRoomOptions { + fn default() -> Self { + Self { + grants: VideoGrants { room_join: true, ..Default::default() }, + room: Default::default(), + } + } +} + +impl From for TestRoomOptions { + fn from(room: RoomOptions) -> Self { + Self { room, ..Default::default() } + } +} + +impl From for TestRoomOptions { + fn from(grants: VideoGrants) -> Self { + Self { grants, ..Default::default() } + } +} + /// Creates the specified number of connections to a shared room for testing. pub async fn test_rooms(count: usize) -> Result)>> { - test_rooms_with_options((0..count).map(|_| RoomOptions::default())).await + test_rooms_with_options((0..count).map(|_| TestRoomOptions::default())).await } /// Creates multiple connections to a shared room for testing, one for each configuration. pub async fn test_rooms_with_options( - options: impl IntoIterator, + options: impl IntoIterator, ) -> Result)>> { let test_env = TestEnvironment::from_env_or_defaults(); let room_name = format!("test_room_{}", create_random_uuid()); @@ -59,17 +88,17 @@ pub async fn test_rooms_with_options( let tokens = options .into_iter() .enumerate() - .map(|(id, options)| -> Result<(String, RoomOptions)> { - let grants = - VideoGrants { room_join: true, room: room_name.clone(), ..Default::default() }; + .map(|(id, mut options)| -> Result<(String, RoomOptions)> { + options.grants.room = room_name.clone(); + let token = AccessToken::with_api_key(&test_env.api_key, &test_env.api_secret) .with_ttl(Duration::from_secs(30 * 60)) // 30 minutes - .with_grants(grants) + .with_grants(options.grants) .with_identity(&format!("p{}", id)) .with_name(&format!("Participant {}", id)) .to_jwt() .context("Failed to generate JWT")?; - Ok((token, options)) + Ok((token, options.room)) }) .collect::>>()?; diff --git a/livekit/tests/data_channel_encryption.rs b/livekit/tests/data_channel_encryption.rs index 2e1515423..f36207295 100644 --- a/livekit/tests/data_channel_encryption.rs +++ b/livekit/tests/data_channel_encryption.rs @@ -49,7 +49,7 @@ async fn test_data_channel_encryption() -> Result<()> { options2.encryption = Some(E2eeOptions { key_provider: key_provider2, encryption_type: EncryptionType::Gcm }); - let mut rooms = test_rooms_with_options([options1, options2]).await?; + let mut rooms = test_rooms_with_options([options1.into(), options2.into()]).await?; let (sending_room, _) = rooms.pop().unwrap(); let (receiving_room, mut receiving_event_rx) = rooms.pop().unwrap(); diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs new file mode 100644 index 000000000..b9424d4db --- /dev/null +++ b/livekit/tests/data_track_test.rs @@ -0,0 +1,464 @@ +// 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. + +#[cfg(feature = "__lk-e2e-test")] +use { + anyhow::{anyhow, Ok, Result}, + common::{test_rooms, test_rooms_with_options, TestRoomOptions}, + futures_util::StreamExt, + livekit::{prelude::*, SimulateScenario}, + livekit_api::access_token::VideoGrants, + std::time::{Duration, Instant}, + test_case::test_case, + tokio::{ + time::{self, timeout}, + try_join, + }, +}; + +mod common; + +#[cfg(feature = "__lk-e2e-test")] +#[test_case(120., 8_192 ; "high_fps_single_packet")] +#[test_case(10., 196_608 ; "low_fps_multi_packet")] +#[test_log::test(tokio::test)] +async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { + // How long to publish frames for. + const PUBLISH_DURATION: Duration = Duration::from_secs(5); + + // Percentage of total frames that must be received on the subscriber end in + // order for the test to pass. + const MIN_PERCENTAGE: f32 = 0.95; + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + let pub_identity = pub_room.local_participant().identity(); + + let frame_count = (PUBLISH_DURATION.as_secs_f64() * publish_fps).round() as u64; + log::info!("Publishing {} frames", frame_count); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await?; + log::info!("Track published"); + + assert!(track.is_published()); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), "my_track"); + + let sleep_duration = Duration::from_secs_f64(1.0 / publish_fps as f64); + for index in 0..frame_count { + track.try_push(vec![index as u8; payload_len].into())?; + time::sleep(sleep_duration).await; + } + Ok(()) + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; + + log::info!("Got remote track: {}", track.info().sid()); + assert!(track.is_published()); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), "my_track"); + assert_eq!(track.publisher_identity(), pub_identity.as_str()); + + let mut subscription = track.subscribe().await?; + + let mut recv_count = 0; + while let Some(frame) = subscription.next().await { + let payload = frame.payload(); + if let Some(first_byte) = payload.first() { + assert!(payload.iter().all(|byte| byte == first_byte)); + } + assert_eq!(frame.user_timestamp(), None); + recv_count += 1; + } + + let recv_percent = recv_count as f32 / frame_count as f32; + log::info!("Received {}/{} frames ({:.2}%)", recv_count, frame_count, recv_percent * 100.); + + if recv_percent < MIN_PERCENTAGE { + Err(anyhow!("Not enough frames received"))?; + } + Ok(()) + }; + timeout(PUBLISH_DURATION + Duration::from_secs(25), async { try_join!(publish, subscribe) }) + .await??; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_many_tracks() -> Result<()> { + const TRACK_COUNT: usize = 256; + + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + let publish_tracks = async { + let mut tracks = Vec::with_capacity(TRACK_COUNT); + let start = Instant::now(); + + for idx in 0..TRACK_COUNT { + let name = format!("track_{}", idx); + let track = room.local_participant().publish_data_track(name.clone()).await?; + + assert!(track.is_published()); + assert_eq!(track.info().name(), name); + + tracks.push(track); + } + + let elapsed = start.elapsed(); + log::info!( + "Publishing {} tracks took {:.2?} (average {:.2?} per track)", + TRACK_COUNT, + elapsed, + elapsed / TRACK_COUNT as u32 + ); + Ok(tracks) + }; + + let tracks = timeout(Duration::from_secs(5), publish_tracks).await??; + for track in &tracks { + // Publish a single large frame per track. + track.try_push(vec![0xFA; 196_608].into())?; + } + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_unauthorized() -> Result<()> { + let (room, _) = test_rooms_with_options([TestRoomOptions { + grants: VideoGrants { room_join: true, can_publish_data: false, ..Default::default() }, + ..Default::default() + }]) + .await? + .pop() + .unwrap(); + + let result = room.local_participant().publish_data_track("my_track").await; + assert!(matches!(result, Err(PublishError::NotAllowed))); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_duplicate_name() -> Result<()> { + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + #[allow(unused)] + let first = room.local_participant().publish_data_track("first").await?; + + let second_result = room.local_participant().publish_data_track("first").await; + assert!(matches!(second_result, Err(PublishError::DuplicateName))); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_e2ee() -> Result<()> { + use livekit::e2ee::{ + key_provider::{KeyProvider, KeyProviderOptions}, + EncryptionType, + }; + use livekit::E2eeOptions; + + const SHARED_SECRET: &[u8] = b"password"; + + let key_provider1 = + KeyProvider::with_shared_key(KeyProviderOptions::default(), SHARED_SECRET.to_vec()); + + let mut options1 = RoomOptions::default(); + options1.encryption = + Some(E2eeOptions { key_provider: key_provider1, encryption_type: EncryptionType::Gcm }); + + let key_provider2 = + KeyProvider::with_shared_key(KeyProviderOptions::default(), SHARED_SECRET.to_vec()); + + let mut options2 = RoomOptions::default(); + options2.encryption = + Some(E2eeOptions { key_provider: key_provider2, encryption_type: EncryptionType::Gcm }); + + let mut rooms = test_rooms_with_options([options1.into(), options2.into()]).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); + + pub_room.e2ee_manager().set_enabled(true); + sub_room.e2ee_manager().set_enabled(true); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await?; + assert!(track.info().uses_e2ee()); + + for index in 0..5 { + track.try_push(vec![index as u8; 196_608].into())?; + time::sleep(Duration::from_millis(25)).await; + } + Ok(()) + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; + + assert!(track.info().uses_e2ee()); + let mut subscription = track.subscribe().await?; + + while let Some(frame) = subscription.next().await { + let payload = frame.payload(); + if let Some(first_byte) = payload.first() { + assert!(payload.iter().all(|byte| byte == first_byte)); + } + } + Ok(()) + }; + timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_published_state() -> Result<()> { + // How long to leave the track published. + const PUBLISH_DURATION: Duration = Duration::from_millis(500); + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await?; + + assert!(track.is_published()); + time::sleep(PUBLISH_DURATION).await; + track.unpublish(); + + Ok(()) + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; + assert!(track.is_published()); + + let elapsed = { + let start = Instant::now(); + track.wait_for_unpublish().await; + start.elapsed() + }; + assert!(elapsed.abs_diff(PUBLISH_DURATION) <= Duration::from_millis(20)); + assert!(!track.is_published()); + + Ok(()) + }; + + timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_resubscribe() -> Result<()> { + const ITERATIONS: usize = 10; + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + _ = track.try_push(vec![0xFA; 64].into()); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + + let mut successful_subscriptions = 0; + for _ in 0..ITERATIONS { + let mut stream = track.subscribe().await.unwrap(); + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + successful_subscriptions += 1; + break; + } + std::mem::drop(stream); + time::sleep(Duration::from_millis(50)).await; + } + assert_eq!(successful_subscriptions, ITERATIONS); + }; + + let _ = timeout(Duration::from_secs(5), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_frame_with_user_timestamp() -> Result<()> { + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + let frame = DataTrackFrame::new(vec![0xFA; 64]).with_user_timestamp_now(); + _ = track.try_push(frame); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + + let mut stream = track.subscribe().await.unwrap(); + let mut got_frame = false; + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + let duration = frame.duration_since_timestamp().expect("Missing timestamp"); + assert!(duration.as_millis() < 1000); + got_frame = true; + break; + } + if !got_frame { + panic!("No frame received"); + } + }; + + let _ = timeout(Duration::from_secs(5), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_case(SimulateScenario::SignalReconnect; "signal_reconnect")] +#[test_case(SimulateScenario::ForceTcp; "full_reconnect")] +#[test_log::test(tokio::test)] +async fn test_subscriber_side_fault(scenario: SimulateScenario) -> Result<()> { + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + _ = track.try_push(vec![0xFA; 64].into()); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + let mut stream = track.subscribe().await.unwrap(); + + // TODO: this should also evaluate what happens if a track subscription is removed + // during a full reconnect event. + sub_room.simulate_scenario(scenario).await.unwrap(); + assert!(track.is_published()); + + let mut got_frame = false; + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + got_frame = true; + break; + } + if !got_frame { + panic!("No frame received"); + } + }; + + let _ = timeout(Duration::from_secs(15), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_case(SimulateScenario::SignalReconnect; "signal_reconnect")] +#[test_case(SimulateScenario::ForceTcp; "full_reconnect")] +#[test_log::test(tokio::test)] +async fn test_publisher_side_fault(scenario: SimulateScenario) -> Result<()> { + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + + pub_room.simulate_scenario(scenario).await.unwrap(); + assert!(track.is_published()); + + loop { + track + .try_push(vec![0xFA; 64].into()) + .expect("Should be able to push frame after reconnect"); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + let mut stream = track.subscribe().await.unwrap(); + + let mut got_frame = false; + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + got_frame = true; + break; + } + if !got_frame { + panic!("No frame received"); + } + }; + + let _ = timeout(Duration::from_secs(10), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + +/// Waits for the first remote data track to be published. +#[cfg(feature = "__lk-e2e-test")] +async fn wait_for_remote_track( + rx: &mut tokio::sync::mpsc::UnboundedReceiver, +) -> Result { + while let Some(event) = rx.recv().await { + if let RoomEvent::RemoteDataTrackPublished(track) = event { + return Ok(track); + } + } + Err(anyhow!("No track published")) +}