diff --git a/Cargo.lock b/Cargo.lock index 0e6725cd..1069f025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -43,7 +58,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "once_cell", "version_check", ] @@ -62,9 +77,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -122,29 +137,32 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.11" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +dependencies = [ + "backtrace", +] [[package]] name = "api" @@ -156,12 +174,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" -dependencies = [ - "rustversion", -] +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argon2" @@ -211,7 +226,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", "synstructure", ] @@ -223,7 +238,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -245,7 +260,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -256,7 +271,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -280,9 +295,9 @@ version = "0.8.1" dependencies = [ "anyhow", "api", - "axum 0.8.8", + "axum 0.8.6", "headers", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "jsonwebtoken", "serde", @@ -310,10 +325,10 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-util", "itoa", "matchit 0.7.3", @@ -336,18 +351,18 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core 0.5.6", + "axum-core 0.5.5", "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-util", "itoa", "matchit 0.8.4", @@ -377,7 +392,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", @@ -391,13 +406,13 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.6" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", @@ -410,22 +425,22 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.12.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum 0.8.8", - "axum-core 0.5.6", + "axum 0.8.6", + "axum-core 0.5.5", "bytes", "form_urlencoded", - "futures-core", "futures-util", "headers", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", + "rustversion", "serde_core", "serde_html_form 0.2.8", "serde_path_to_error", @@ -445,9 +460,9 @@ dependencies = [ "axum 0.7.9", "bytes", "cookie", - "http 1.4.0", + "http 1.3.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-util", "mime", "pretty_assertions", @@ -473,6 +488,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -493,15 +523,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bigdecimal" -version = "0.4.10" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" dependencies = [ "autocfg", "libm", @@ -520,6 +550,29 @@ dependencies = [ "serde", ] +[[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.107", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -528,11 +581,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ - "serde_core", + "serde", ] [[package]] @@ -582,9 +635,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ "borsh-derive", "cfg_aliases", @@ -592,31 +645,31 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" @@ -648,9 +701,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -675,9 +728,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "shlex", @@ -695,6 +748,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -744,9 +806,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", @@ -767,11 +829,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.60" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -779,9 +852,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -791,21 +864,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cli" @@ -817,6 +890,15 @@ dependencies = [ "webrtc", ] +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -843,7 +925,7 @@ dependencies = [ "json5", "nom", "pathdiff", - "ron 0.8.1", + "ron", "rust-ini 0.20.0", "serde", "serde_json", @@ -853,21 +935,21 @@ dependencies = [ [[package]] name = "config" -version = "0.15.19" +version = "0.15.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" dependencies = [ "async-trait", "convert_case", "json5", "pathdiff", - "ron 0.12.0", + "ron", "rust-ini 0.21.3", "serde-untagged", "serde_core", "serde_json", - "toml 0.9.12+spec-1.1.0", - "winnow", + "toml 0.9.8", + "winnow 0.7.13", "yaml-rust2 0.10.4", ] @@ -892,7 +974,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -951,9 +1033,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.4.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -1017,9 +1099,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1059,7 +1141,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -1082,7 +1164,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -1093,14 +1175,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" @@ -1129,9 +1211,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", "serde_core", @@ -1139,23 +1221,22 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "rustc_version", - "syn 2.0.117", + "syn 2.0.107", "unicode-xid", ] @@ -1185,7 +1266,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -1288,9 +1369,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.10" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", "serde_core", @@ -1359,9 +1440,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fixedbitset" @@ -1425,9 +1506,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1440,9 +1521,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1450,15 +1531,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1478,38 +1559,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1519,14 +1600,15 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1535,9 +1617,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -1584,6 +1666,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -1658,9 +1746,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" @@ -1689,7 +1777,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.4.0", + "http 1.3.1", "httpdate", "mime", "sha1", @@ -1701,7 +1789,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.4.0", + "http 1.3.1", ] [[package]] @@ -1754,11 +1842,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.12" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1774,11 +1862,12 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", + "fnv", "itoa", ] @@ -1800,7 +1889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.3.1", ] [[package]] @@ -1811,7 +1900,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] @@ -1820,7 +1909,7 @@ dependencies = [ name = "http-log" version = "0.8.1" dependencies = [ - "axum 0.8.8", + "axum 0.8.6", "http-body-util", "tracing", ] @@ -1868,15 +1957,15 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", @@ -1894,35 +1983,36 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http 1.3.1", + "hyper 1.7.0", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.3", ] [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -1930,9 +2020,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.65" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1959,7 +2049,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "hmac", - "http 1.4.0", + "http 1.3.1", "reqwest", "serde", "serde_json", @@ -1969,9 +2059,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", @@ -1982,9 +2072,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1995,10 +2085,11 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2009,38 +2100,42 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", + "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", @@ -2083,12 +2178,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -2101,7 +2196,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -2136,15 +2231,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ "memchr", "serde", @@ -2152,15 +2247,15 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" @@ -2185,7 +2280,7 @@ checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -2205,9 +2300,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -2232,7 +2327,7 @@ checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "getrandom 0.2.17", + "getrandom 0.2.16", "hmac", "js-sys", "p256", @@ -2255,7 +2350,7 @@ checksum = "9be8f8058681ab8b7e4959a1ee88a4446f43b7c787f174d9d1b32ba493737a59" dependencies = [ "bytes", "log", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -2267,6 +2362,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2275,26 +2376,35 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libm" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", "libc", - "plain", - "redox_syscall 0.7.3", + "redox_syscall", ] [[package]] @@ -2313,13 +2423,19 @@ name = "libwish" version = "0.8.1" dependencies = [ "anyhow", - "http 1.4.0", + "http 1.3.1", "parse_link_header", "reqwest", "url", "webrtc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2328,9 +2444,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "live777" @@ -2338,11 +2454,11 @@ version = "0.8.1" dependencies = [ "anyhow", "api", - "axum 0.8.8", + "axum 0.8.6", "axum-extra", "clap", "cli", - "http 1.4.0", + "http 1.3.1", "livecam", "liveion", "liveman", @@ -2357,7 +2473,7 @@ dependencies = [ "tempfile", "tokio", "tokio-util", - "toml 1.0.6+spec-1.1.0", + "toml 0.9.8", "tracing", "tracing-subscriber", ] @@ -2375,7 +2491,7 @@ dependencies = [ "bytes", "chrono", "cli", - "config 0.15.19", + "config 0.15.18", "iceserver", "jsonwebtoken", "libc", @@ -2389,7 +2505,7 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "toml 1.0.6+spec-1.1.0", + "toml 1.0.7+spec-1.1.0", "tower", "tower-http", "tracing", @@ -2405,20 +2521,23 @@ dependencies = [ "async-stream", "async-trait", "auth", - "axum 0.8.8", + "axum 0.8.6", "axum-extra", + "base64 0.22.1", "byteorder", "bytes", "chrono", "clap", + "cmake", "fs2", "glob", "h264-reader", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-log", "iceserver", "lazy_static", + "libc", "libwish", "livetwo", "md-5", @@ -2426,6 +2545,7 @@ dependencies = [ "net4mqtt", "once_cell", "opendal", + "pkg-config", "prometheus", "rand 0.10.0", "reqwest", @@ -2452,12 +2572,12 @@ dependencies = [ "anyhow", "api", "auth", - "axum 0.8.8", + "axum 0.8.6", "axum-extra", "chrono", "clap", "glob", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "http-log", @@ -2480,6 +2600,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "livesrc" +version = "0.8.1" +dependencies = [ + "anyhow", + "axum 0.8.6", + "bytes", + "clap", + "cli", + "rand 0.8.5", + "reqwest", + "serde", + "tokio", + "toml 0.9.8", + "tower-http", + "tracing", + "tracing-subscriber", + "v4l", + "webrtc", +] + [[package]] name = "livetwo" version = "0.8.1" @@ -2513,9 +2654,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-slab" @@ -2562,9 +2703,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -2647,11 +2788,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "1.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", @@ -2682,7 +2832,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.0", + "http 1.3.1", "httparse", "memchr", "mime", @@ -2753,10 +2903,11 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ + "byteorder", "lazy_static", "libm", "num-integer", @@ -2769,9 +2920,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" @@ -2822,6 +2973,15 @@ dependencies = [ "scuffle-workspace-hack", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -2839,9 +2999,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -2861,14 +3021,14 @@ dependencies = [ "bytes", "crc32c", "futures", - "getrandom 0.2.17", - "http 1.4.0", + "getrandom 0.2.16", + "http 1.3.1", "http-body 1.0.1", "jiff", "log", "md-5", "percent-encoding", - "quick-xml 0.38.4", + "quick-xml 0.38.3", "reqsign", "reqwest", "serde", @@ -2918,7 +3078,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -2969,7 +3129,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2980,7 +3140,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc23fdb8bbf668d582b0c17120bf6b7f91d85ccad3a5b39706f019a4efda005" dependencies = [ - "http 1.4.0", + "http 1.3.1", "lazy_static", "regex", ] @@ -3002,6 +3162,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "3.0.6" @@ -3029,9 +3195,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", "ucd-trie", @@ -3039,9 +3205,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -3049,22 +3215,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -3081,9 +3247,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3118,12 +3284,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "poly1305" version = "0.8.0" @@ -3149,15 +3309,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -3173,9 +3333,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -3212,7 +3372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -3226,11 +3386,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.5.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.25.4+spec-1.1.0", + "toml_edit 0.23.7", ] [[package]] @@ -3252,14 +3412,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -3272,7 +3432,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", "version_check", "yansi", ] @@ -3289,7 +3449,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -3359,9 +3519,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", "serde", @@ -3378,10 +3538,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3398,11 +3558,11 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3417,16 +3577,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -3467,7 +3627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -3498,7 +3658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -3507,14 +3667,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.4", ] @@ -3531,7 +3691,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", ] [[package]] @@ -3554,23 +3714,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -3580,9 +3731,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -3591,9 +3742,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rend" @@ -3615,11 +3766,11 @@ dependencies = [ "base64 0.22.1", "chrono", "form_urlencoded", - "getrandom 0.2.17", + "getrandom 0.2.16", "hex", "hmac", "home", - "http 1.4.0", + "http 1.3.1", "log", "percent-encoding", "quick-xml 0.37.5", @@ -3635,18 +3786,18 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.28" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-rustls", "hyper-util", "js-sys", @@ -3671,16 +3822,16 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.3", ] [[package]] name = "reserve-port" -version = "2.4.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -3711,7 +3862,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -3719,9 +3870,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.46" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -3737,9 +3888,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.46" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -3753,30 +3904,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.11.0", + "bitflags 2.9.4", "serde", "serde_derive", ] -[[package]] -name = "ron" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" -dependencies = [ - "bitflags 2.11.0", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", -] - [[package]] name = "rsa" -version = "0.9.10" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", @@ -3880,7 +4017,7 @@ dependencies = [ "flume", "futures-util", "log", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-util", @@ -3915,11 +4052,11 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.11.0" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" dependencies = [ - "axum 0.8.8", + "axum 0.8.6", "mime_guess", "rust-embed-impl", "rust-embed-utils", @@ -3929,22 +4066,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.11.0" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.117", + "syn 2.0.107", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.11.0" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" dependencies = [ "sha2", "walkdir", @@ -3988,9 +4125,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.40.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", "borsh", @@ -4002,6 +4139,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4026,24 +4175,37 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "once_cell", "ring", @@ -4055,9 +4217,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", "zeroize", @@ -4065,9 +4227,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -4082,9 +4244,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -4129,7 +4291,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b04b276c2f79846b7968abe6f87cedf951e06fd2a2b72d99c457e85d7e40f3fb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", "byteorder", "bytes", "nutype-enum", @@ -4175,14 +4337,14 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "sea-orm" -version = "1.1.19" +version = "1.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d945f62558fac19e5988680d2fdf747b734c2dbc6ce2cb81ba33ed8dde5b103" +checksum = "699b1ec145a6530c8f862eed7529d8a6068392e628d81cc70182934001e9c2a3" dependencies = [ "async-stream", "async-trait", @@ -4201,7 +4363,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tracing", "url", @@ -4210,9 +4372,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "1.1.19" +version = "1.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94492e2ab6c045b4cc38013809ce255d14c3d352c9f0d11e6b920e2adc948ad" +checksum = "500cd31ebb07814d4c7b73796708bfab6c13d22f8db072cdb5115f967f4d5d2c" dependencies = [ "chrono", "clap", @@ -4226,23 +4388,23 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.19" +version = "1.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c2e64a50a9cc8339f10a27577e10062c7f995488e469f2c95762c5ee847832" +checksum = "b0c964f4b7f34f53decf381bc88f03187b9355e07f356ce65544626e781a9585" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "sea-bae", - "syn 2.0.117", + "syn 2.0.107", "unicode-ident", ] [[package]] name = "sea-orm-migration" -version = "1.1.19" +version = "1.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7315c0cadb7e60fb17ee2bb282aa27d01911fc2a7e5836ec1d4ac37d19250bb4" +checksum = "977e3f71486b04371026d1ecd899f49cf437f832cd11d463f8948ee02e47ed9e" dependencies = [ "async-trait", "clap", @@ -4297,8 +4459,8 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.117", - "thiserror 2.0.18", + "syn 2.0.107", + "thiserror 2.0.17", ] [[package]] @@ -4321,7 +4483,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -4389,7 +4551,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -4420,15 +4582,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", + "ryu", "serde", "serde_core", - "zmij", ] [[package]] @@ -4528,11 +4690,10 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.8" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ - "errno", "libc", ] @@ -4554,13 +4715,13 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "simple_asn1" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -4572,9 +4733,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -4606,12 +4767,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4700,7 +4861,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -4720,7 +4881,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -4743,7 +4904,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.117", + "syn 2.0.107", "tokio", "url", ] @@ -4757,7 +4918,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.11.0", + "bitflags 2.9.4", "byteorder", "bytes", "chrono", @@ -4788,7 +4949,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -4804,7 +4965,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.11.0", + "bitflags 2.9.4", "byteorder", "chrono", "crc", @@ -4831,7 +4992,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -4858,7 +5019,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tracing", "url", @@ -4886,7 +5047,7 @@ dependencies = [ "opendal", "serde", "tokio", - "toml 1.0.6+spec-1.1.0", + "toml 1.0.7+spec-1.1.0", "tracing", ] @@ -4959,9 +5120,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -4985,7 +5146,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -4996,14 +5157,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -5018,11 +5179,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.17", ] [[package]] @@ -5033,18 +5194,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -5058,30 +5219,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -5098,9 +5259,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -5123,9 +5284,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", @@ -5133,20 +5294,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -5161,9 +5322,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -5172,10 +5333,12 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ + "async-stream", + "bytes", "futures-core", "tokio", "tokio-stream", @@ -5208,30 +5371,32 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ + "indexmap", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "toml_writer", + "winnow 0.7.13", ] [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "indexmap", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.0", ] [[package]] @@ -5245,18 +5410,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] @@ -5272,28 +5437,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -5304,15 +5469,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", @@ -5326,16 +5491,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.9.4", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "http-range-header", @@ -5367,9 +5532,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.44" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -5379,20 +5544,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -5411,9 +5576,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", @@ -5473,9 +5638,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.9.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" @@ -5485,24 +5650,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-segmentation" @@ -5534,9 +5699,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.8" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -5558,17 +5723,37 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.3.4", "js-sys", - "rand 0.10.0", - "serde_core", + "rand 0.9.2", + "serde", "wasm-bindgen", ] +[[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", +] + [[package]] name = "valuable" version = "0.1.1" @@ -5623,11 +5808,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", ] [[package]] @@ -5636,7 +5821,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -5647,9 +5832,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -5658,14 +5843,27 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-shared", +] + [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", - "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5674,9 +5872,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5684,22 +5882,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -5745,7 +5943,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.9.4", "hashbrown 0.15.5", "indexmap", "semver", @@ -5753,9 +5951,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -5777,14 +5975,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -6005,6 +6203,18 @@ dependencies = [ "winapi", ] +[[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 = "whoami" version = "1.6.1" @@ -6067,7 +6277,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -6078,7 +6288,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -6123,6 +6333,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -6329,13 +6548,25 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -6366,7 +6597,7 @@ dependencies = [ "heck 0.5.0", "indexmap", "prettyplease", - "syn 2.0.117", + "syn 2.0.107", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -6382,7 +6613,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -6394,7 +6625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.9.4", "indexmap", "log", "serde", @@ -6426,9 +6657,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -6508,10 +6739,11 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -6519,34 +6751,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] @@ -6566,7 +6798,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", "synstructure", ] @@ -6581,20 +6813,20 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", @@ -6603,9 +6835,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -6614,17 +6846,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.107", ] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index a0abd8d4..fa8c5c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ members = [ "liveman", "livetwo", "livecam", + "livesrc", ] [workspace.package] @@ -71,7 +72,7 @@ webrtc = { git = "https://github.com/webrtc-rs/webrtc", rev = "4da5280" } sdp = { git = "https://github.com/webrtc-rs/webrtc", rev = "4da5280" } axum = "0.8" -axum-extra = "0.12" +axum-extra = "0.10" tower-http = "0.6" rust-embed = "8.7" mime_guess = "2.0" @@ -124,15 +125,15 @@ serde_json = { workspace = true } anyhow = { workspace = true } opendal = "0.55.0" -toml = "1.0" +toml = "0.9" tracing-subscriber = { version = "0.3", features = ["env-filter"] } [features] webui = ["liveion/webui", "liveman/webui", "livecam/webui"] net4mqtt = ["liveion/net4mqtt", "liveman/net4mqtt"] recorder = ["liveion/recorder", "liveman/recorder"] - -source = ["liveion/source"] source-sdp = ["liveion/source-sdp"] source-rtsp = ["liveion/source-rtsp"] +source-rtp = ["liveion/source-rtp"] +source-libcamera = ["liveion/source-libcamera"] source-all = ["liveion/source-all"] diff --git a/build_all.sh b/build_all.sh new file mode 100644 index 00000000..c3bd5f75 --- /dev/null +++ b/build_all.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +echo "=======================================" +echo " Live777 Dual-Platform Builder" +echo "=======================================" + +# 1. 编译 Raspberry Pi 版本 +echo ">>> Building for Raspberry Pi (live777_pi)..." +unset CXXFLAGS +cargo clean -p liveion # 确保底层 C++ 构建宏被清理 +cargo build --release -j 2 --features source-libcamera,webui +cp target/release/live777 live777_pi +echo "[OK] Pi Output is ready: ./live777_pi" + +echo "---------------------------------------" + +# 2. 编译 RDK X5 版本 +echo ">>> Building for D-Robotics RDK X5 (live777_rdk)..." +export CXXFLAGS="-DPLATFORM_RDK=ON" +cargo clean -p liveion # 确保底层 C++ 构建宏被重新触发 +cargo build --release -j 2 --features source-libcamera,webui +cp target/release/live777 live777_rdk +echo "[OK] RDK Output is ready: ./live777_rdk" + +echo "=======================================" +echo "Done! Please use live777_pi for Raspberry Pi and live777_rdk for RDK X5." diff --git a/conf/live777.toml b/conf/live777.toml index be1e4078..c2d22874 100644 --- a/conf/live777.toml +++ b/conf/live777.toml @@ -18,15 +18,14 @@ url = "udp://0.0.0.0:8891?host=127.0.0.1&port=8890" # listen = "[::]:7777" # Cross-Origin Resource Sharing (CORS) # reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS -# cors = false +cors = true -[[ice_servers]] -urls = [ - "stun:stun.22333.fun", - "stun:cn.22333.fun", - "stun:stun.l.google.com:19302", + [[ice_servers]] + urls = [ + "stun:stun.22333.fun", + "stun:cn.22333.fun", + "stun:stun.l.google.com:19302", ] - [stream] # Need enabled `--features=source` @@ -39,6 +38,11 @@ urls = [ # - RTSP: rtsp://username:password@host:port/path # url = "rtsp://username:password@host:port/path" +# 核心测试:使用原生 libcamera-bridge 方案 (普适性标准方案) +[[stream.sources]] +stream_id = "pi_cam" +url = "libcamera://0?width=640&height=480&fps=30" + [recorder] # Stream name patterns for auto-recording, supports wildcards # auto_streams = ["*"] # Record all streams diff --git a/conf/livesrc-rdk.toml b/conf/livesrc-rdk.toml new file mode 100644 index 00000000..329f28b9 --- /dev/null +++ b/conf/livesrc-rdk.toml @@ -0,0 +1,19 @@ +[http] +listen = "0.0.0.0:7777" +cors = true + +[[ice_servers]] +urls = [ + "stun:stun.l.google.com:19302", +] + +[stream] +[[stream.sources]] +stream_id = "usbcam" +# RDK X5 专用 V4L2 配置: +# 1. 务必确认 /dev/video0 是出流节点,而非 metadata 节点(可用 v4l2-ctl --list-devices 确认) +# 2. 建议通过 v4l2-ctl 关闭自动曝光,或码率设为 1.5Mbps 防止花屏 +url = "v4l2:///dev/video0?width=640&height=480&fps=30&bitrate=1500000" + +[log] +level = "info" diff --git a/conf/livesrc-rpicam.toml b/conf/livesrc-rpicam.toml new file mode 100644 index 00000000..df73a8b7 --- /dev/null +++ b/conf/livesrc-rpicam.toml @@ -0,0 +1,25 @@ +# Live777 最终对齐版配置文件 (修正字段名为 stream_id) + +[http] +listen = "0.0.0.0:7777" + +[log] +level = "debug" + +# 严格匹配 liveion/src/config.rs 中的构造 +[stream] +[[stream.sources]] +stream_id = "camera" +url = "libcamera://0?width=640&height=480&fps=30" + +# 同时保留原有的 paths 结构以确保万一 +[paths.camera] +source = "libcamera" +source_on_demand = false +rtp_port = 5004 + +[paths.camera.libcamera] +width = 640 +height = 480 +fps = 30 +camera_id = 0 diff --git a/liveion/Cargo.toml b/liveion/Cargo.toml index 28343a3a..f498a818 100644 --- a/liveion/Cargo.toml +++ b/liveion/Cargo.toml @@ -32,15 +32,16 @@ reqwest = { workspace = true } rand = "0.10" serde = { workspace = true, features = ["serde_derive"] } serde_json = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["process"] } tokio-stream = "0.1.15" async-stream = "0.3.5" tracing = { workspace = true } webrtc = { workspace = true } uuid = { workspace = true } +libc = "0.2" async-trait = "0.1" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } lazy_static = "1.4.0" md-5 = "0.10.6" prometheus = "0.14" @@ -55,6 +56,7 @@ scuffle-h265 = { version = "0.2.2", optional = true } glob = "0.3" url = { workspace = true, optional = true } fs2 = "0.4" +base64 = { workspace = true } [features] webui = ["dep:rust-embed", "dep:mime_guess"] @@ -72,7 +74,13 @@ recorder = [ source = ["dep:rtsp", "dep:url", "dep:bytes"] source-sdp = ["source"] source-rtsp = ["source"] -source-all = ["source-sdp", "source-rtsp"] +source-rtp = ["source"] +source-libcamera = ["source"] +source-all = ["source-sdp", "source-rtsp", "source-rtp", "source-libcamera"] [dev-dependencies] tempfile = "3.26" + +[build-dependencies] +cmake = "0.1" +pkg-config = "0.3" diff --git a/liveion/build.rs b/liveion/build.rs new file mode 100644 index 00000000..d2cf56f8 --- /dev/null +++ b/liveion/build.rs @@ -0,0 +1,85 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + // Only run if the source-libcamera feature is enabled + if env::var("CARGO_FEATURE_SOURCE_LIBCAMERA").is_err() { + return; + } + + let target = env::var("TARGET").unwrap_or_default(); + let is_rdk = target.contains("aarch64"); + + // 1. Rerun-if-changed for all platforms + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/bridge_ffi.h"); + if is_rdk { + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/encoder_rdk.cpp"); + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/v4l2_capture_rdk.cpp"); + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/bridge_v4l2_rdk_ffi.cpp"); + } else { + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/camera.cpp"); + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/encoder.cpp"); + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/v4l2_capture.cpp"); + println!("cargo:rerun-if-changed=../livesrc/libcamera-bridge/bridge_v4l2_ffi.cpp"); + } + + // 2. Setup Sysroot handles + if is_rdk { + if let Ok(sysroot) = env::var("RDK_SYSROOT") { + let sysroot = PathBuf::from(sysroot); + println!("cargo:rustc-link-search=native={}", sysroot.join("usr/lib").display()); + println!("cargo:rustc-link-search=native={}", sysroot.join("lib").display()); + unsafe { env::set_var("PKG_CONFIG_ALLOW_CROSS", "1"); } + } + } else if let Ok(sysroot) = env::var("PI_SYSROOT") { + let sysroot = PathBuf::from(sysroot); + let pkg_config_path = sysroot.join("usr/lib/arm-linux-gnueabihf/pkgconfig"); + unsafe { + env::set_var("PKG_CONFIG_SYSROOT_DIR", &sysroot); + env::set_var("PKG_CONFIG_PATH", pkg_config_path); + env::set_var("PKG_CONFIG_ALLOW_CROSS", "1"); + } + println!("cargo:rustc-link-search=native={}", sysroot.join("usr/lib/arm-linux-gnueabihf").display()); + } + + // 3. Find libcamera (RPi Only) + if !is_rdk { + let mut config = pkg_config::Config::new(); + config.atleast_version("0.1"); + if let Ok(lib) = config.probe("libcamera") { + for path in lib.include_paths { println!("cargo:include={}", path.display()); } + } + } + + // 4. Build the C++ bridge library using CMake + let mut cmake_config = cmake::Config::new("../livesrc/libcamera-bridge"); + cmake_config.define("CMAKE_POSITION_INDEPENDENT_CODE", "ON"); + + if is_rdk { + cmake_config.define("PLATFORM_RDK", "ON"); + } + + let dst = cmake_config.build(); + + println!("cargo:rustc-link-search=native={}/lib", dst.display()); + println!("cargo:rustc-link-lib=static=cambridge"); + + // 5. Link CORE dependencies + println!("cargo:rustc-link-lib=dylib=stdc++"); + if is_rdk { + // Direct path for RDK X5 firmware libraries + println!("cargo:rustc-link-search=native=/usr/hobot/lib"); + println!("cargo:rustc-link-search=native=/usr/lib"); + + println!("cargo:rustc-link-lib=dylib=multimedia"); + println!("cargo:rustc-link-lib=dylib=hbmem"); + println!("cargo:rustc-link-lib=dylib=vpf"); + } else { + println!("cargo:rustc-link-lib=dylib=camera"); + println!("cargo:rustc-link-lib=dylib=camera-base"); + println!("cargo:rustc-link-lib=dylib=event"); + } + + println!("cargo:rustc-link-arg=-Wl,--allow-shlib-undefined"); + println!("cargo:rustc-link-arg=-Wl,--unresolved-symbols=ignore-in-shared-libs"); +} diff --git a/liveion/src/config.rs b/liveion/src/config.rs index afa5de6f..2e2445d7 100644 --- a/liveion/src/config.rs +++ b/liveion/src/config.rs @@ -408,9 +408,13 @@ impl SourceConfig { && !url_lower.starts_with("rtsps://") && !url_lower.starts_with("file://") && !url_lower.ends_with(".sdp") + && !url_lower.starts_with("rtp://") + && !url_lower.starts_with("exec://") + && !url_lower.starts_with("libcamera://") + && !url_lower.starts_with("v4l2://") { anyhow::bail!( - "Invalid URL format: {}. Must be rtsp://, rtsps://, file://, or end with .sdp", + "Invalid URL format: {}. Must be rtsp://, rtsps://, file://, rtp://, exec://, libcamera://, v4l2:// or end with .sdp", self.url ); } diff --git a/liveion/src/stream/source/h264_utils.rs b/liveion/src/stream/source/h264_utils.rs new file mode 100644 index 00000000..98afe404 --- /dev/null +++ b/liveion/src/stream/source/h264_utils.rs @@ -0,0 +1,274 @@ +//! H.264 Annex-B parser and RTP packetizer utilities. +//! Optimized for near-zero-copy using the `bytes` crate. + +use bytes::{Buf, Bytes, BytesMut}; + +/// NAL unit type enumeration for H.264. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NalType { + Sps, + Pps, + Aud, + Sei, + Idr, + Slice, + Other(u8), +} + +impl NalType { + pub fn from_header(header_byte: u8) -> Self { + match header_byte & 0x1F { + 1 => NalType::Slice, + 5 => NalType::Idr, + 6 => NalType::Sei, + 7 => NalType::Sps, + 8 => NalType::Pps, + 9 => NalType::Aud, + other => NalType::Other(other), + } + } + + pub fn is_vcl(&self) -> bool { + matches!(self, NalType::Slice | NalType::Idr) + } +} + +#[derive(Debug, Clone)] +pub struct NalUnit { + pub nal_type: NalType, + pub data: Bytes, +} + +pub struct AnnexBParser { + buffer: BytesMut, +} + +impl AnnexBParser { + pub fn new() -> Self { + Self { + buffer: BytesMut::with_capacity(256 * 1024), + } + } + + pub fn push(&mut self, data: &[u8]) { + self.buffer.extend_from_slice(data); + } + + pub fn extract_nals(&mut self) -> Vec { + let mut nals = Vec::new(); + + loop { + let buf = &self.buffer[..]; + if buf.len() < 3 { break; } + + // Find the first start code + if let Some((start_pos, start_len)) = find_start_code(buf) { + if start_pos > 0 { + self.buffer.advance(start_pos); + continue; + } + + // Skip the start code we just found + let search_buf = &self.buffer[start_len..]; + if let Some((next_pos, _next_len)) = find_start_code(search_buf) { + // Current NAL found! + self.buffer.advance(start_len); + let nal_data = self.buffer.split_to(next_pos).freeze(); + + if !nal_data.is_empty() { + let nal_type = NalType::from_header(nal_data[0]); + nals.push(NalUnit { nal_type, data: nal_data }); + } + continue; + } else { + break; + } + } else { + if self.buffer.len() > 1024 * 1024 { self.buffer.clear(); } + break; + } + } + nals + } +} + +fn find_start_code(buf: &[u8]) -> Option<(usize, usize)> { + if buf.len() < 3 { return None; } + for i in 0..buf.len() - 2 { + if buf[i] == 0 && buf[i+1] == 0 { + if buf[i+2] == 1 { return Some((i, 3)); } + if i + 3 < buf.len() && buf[i+2] == 0 && buf[i+3] == 1 { return Some((i, 4)); } + } + } + None +} + +impl Default for AnnexBParser { + fn default() -> Self { Self::new() } +} + +#[derive(Debug, Clone)] +pub struct RtpHeader { + pub version: u8, + pub padding: bool, + pub extension: bool, + pub marker: bool, + pub payload_type: u8, + pub sequence: u16, + pub timestamp: u32, + pub ssrc: u32, +} + +impl RtpHeader { + pub fn to_bytes(&self, buf: &mut [u8]) { + buf[0] = (self.version << 6) | ((self.padding as u8) << 5) | ((self.extension as u8) << 4); + buf[1] = ((self.marker as u8) << 7) | (self.payload_type & 0x7F); + buf[2..4].copy_from_slice(&self.sequence.to_be_bytes()); + buf[4..8].copy_from_slice(&self.timestamp.to_be_bytes()); + buf[8..12].copy_from_slice(&self.ssrc.to_be_bytes()); + } +} + +#[derive(Debug, Clone)] +pub struct RtpPacket { + pub header: RtpHeader, + pub payload: Bytes, +} + +impl RtpPacket { + pub fn to_bytes(&self) -> Bytes { + let mut b = BytesMut::with_capacity(12 + self.payload.len()); + b.extend_from_slice(&[0u8; 12]); + self.header.to_bytes(&mut b[0..12]); + b.extend_from_slice(&self.payload); + b.freeze() + } +} + +pub struct H264Packetizer { + mtu: usize, + payload_type: u8, + ssrc: u32, + sequence: u16, + timestamp: u32, + clock_rate: u32, + cached_sps: Option, + cached_pps: Option, + sps_pps_timestamp: u32, +} + +impl H264Packetizer { + pub fn new(mtu: usize, payload_type: u8, clock_rate: u32) -> Self { + Self { + mtu, payload_type, + ssrc: rand::random(), + sequence: rand::random(), + timestamp: rand::random(), + clock_rate, + cached_sps: None, cached_pps: None, + sps_pps_timestamp: 0, + } + } + + /// Helper for FFI: packetize raw bytes directly + pub fn packetize_raw(&mut self, data: &Bytes) -> Vec { + let nal_type = NalType::from_header(data[0]); + let nal = NalUnit { + nal_type, + data: data.clone(), + }; + self.packetize(&nal) + } + + pub fn packetize(&mut self, nal: &NalUnit) -> Vec { + let mut packets = Vec::new(); + match nal.nal_type { + NalType::Sps => { self.cached_sps = Some(nal.data.clone()); self.sps_pps_timestamp = self.timestamp; } + NalType::Pps => { self.cached_pps = Some(nal.data.clone()); self.sps_pps_timestamp = self.timestamp; } + NalType::Idr => { + if self.sps_pps_timestamp != self.timestamp { + if let Some(sps) = &self.cached_sps { packets.push(self.create_single(sps.clone(), false)); } + if let Some(pps) = &self.cached_pps { packets.push(self.create_single(pps.clone(), false)); } + self.sps_pps_timestamp = self.timestamp; + } + } + _ => {} + } + + if nal.data.len() <= self.mtu - 12 { + packets.push(self.create_single(nal.data.clone(), nal.nal_type.is_vcl())); + } else { + packets.extend(self.create_fua(nal)); + } + packets + } + + fn create_single(&mut self, data: Bytes, marker: bool) -> RtpPacket { + let p = RtpPacket { + header: RtpHeader { + version: 2, padding: false, extension: false, marker, + payload_type: self.payload_type, sequence: self.sequence, + timestamp: self.timestamp, ssrc: self.ssrc, + }, + payload: data, + }; + self.sequence = self.sequence.wrapping_add(1); + p + } + + fn create_fua(&mut self, nal: &NalUnit) -> Vec { + let mut packets = Vec::new(); + let header = nal.data[0]; + let nri = header & 0xE0; + let typ = header & 0x1F; + let payload = nal.data.slice(1..); + let max_size = self.mtu - 12 - 2; + + let mut offset = 0; + while offset < payload.len() { + let start = offset == 0; + let size = std::cmp::min(max_size, payload.len() - offset); + let end = offset + size == payload.len(); + + let mut f_payload = BytesMut::with_capacity(2 + size); + f_payload.extend_from_slice(&[nri | 28, ((start as u8) << 7) | ((end as u8) << 6) | typ]); + f_payload.extend_from_slice(&payload.slice(offset..offset+size)); + + packets.push(RtpPacket { + header: RtpHeader { + version: 2, padding: false, extension: false, marker: end && nal.nal_type.is_vcl(), + payload_type: self.payload_type, sequence: self.sequence, + timestamp: self.timestamp, ssrc: self.ssrc, + }, + payload: f_payload.freeze(), + }); + self.sequence = self.sequence.wrapping_add(1); + offset += size; + } + packets + } + + pub fn advance_timestamp(&mut self, increment: u32) { + self.timestamp = self.timestamp.wrapping_add(increment); + } + + pub fn cached_sps(&self) -> Option { self.cached_sps.clone() } + pub fn cached_pps(&self) -> Option { self.cached_pps.clone() } + + pub fn get_sprop_parameter_sets(&self) -> Option { + use base64::prelude::*; + if let (Some(sps), Some(pps)) = (&self.cached_sps, &self.cached_pps) { + Some(format!("{},{}", BASE64_STANDARD.encode(sps), BASE64_STANDARD.encode(pps))) + } else { + None + } + } + + pub fn get_current_timestamp(&self) -> u32 { + self.timestamp + } +} + +pub fn parse_profile_level_id(sps: &[u8]) -> Option { + if sps.len() >= 4 { Some(format!("{:02x}{:02x}{:02x}", sps[1], sps[2], sps[3])) } else { None } +} diff --git a/liveion/src/stream/source/libcamera_source.rs b/liveion/src/stream/source/libcamera_source.rs new file mode 100644 index 00000000..34c39b1a --- /dev/null +++ b/liveion/src/stream/source/libcamera_source.rs @@ -0,0 +1,318 @@ +//! Libcamera-Bridge Source (FFI Edition). +//! +//! Direct integration with libcamera via C FFI. +//! Eliminates process overhead and IPC latency by linking libcamera-bridge +//! as a static library. + +use super::h264_utils::{H264Packetizer, AnnexBParser, parse_profile_level_id}; +use super::stream_config_v2::LibcameraUrlParams; +use super::{InternalSourceConfig, MediaPacket, StateChangeEvent, StreamSource, StreamSourceState}; +use anyhow::Result; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::{RwLock, broadcast, mpsc}; +use tracing::{info, warn}; +use std::ffi::CStr; +use std::os::raw::{c_char, c_int, c_void}; +use std::time::Instant; + +// SYNC imports with rtsp_source.rs patterns (V14.8-STABLE) +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::RTCPFeedback; +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters}; + +const CHANNEL_VIDEO_RTP: u8 = 0; + +// --- FFI Bindings --- + +#[repr(C)] +struct BridgeContext { _private: [u8; 0] } + +/// Safety wrapper for the raw FFI pointer to allow Send/Sync crossing tokio task boundaries. +#[derive(Clone, Copy)] +pub struct LibcameraPtr(pub *mut BridgeContext); +unsafe impl Send for LibcameraPtr {} +unsafe impl Sync for LibcameraPtr {} + +// V14.8: SYNC WITH bridge_ffi.cpp (5 arguments) +type NALCallbackFFI = unsafe extern "C" fn(data: *const u8, size: usize, is_keyframe: c_int, timestamp: u64, user_data: *mut c_void); + +unsafe extern "C" { + fn bridge_init( + width: c_int, + height: c_int, + fps: c_int, + bitrate: c_int, + camera_id: c_int, + rotation: c_int, + hflip: c_int, + vflip: c_int + ) -> *mut BridgeContext; + + fn bridge_set_callback(ctx: *mut BridgeContext, callback: NALCallbackFFI, user_data: *mut c_void); + fn bridge_start(ctx: *mut BridgeContext) -> bool; + fn bridge_stop(ctx: *mut BridgeContext); + fn bridge_request_keyframe(ctx: *mut BridgeContext); + fn bridge_get_error(ctx: *mut BridgeContext) -> *const c_char; + fn bridge_free(ctx: *mut BridgeContext); +} + +/// Raw message from C++ callback +struct NALMessage { + data: Vec, + is_keyframe: bool, + timestamp_us: u64, +} + +pub struct LibcameraSource { + config: InternalSourceConfig, + params: LibcameraUrlParams, + state: Arc>, + rtp_tx: broadcast::Sender, + state_tx: broadcast::Sender, + task_handles: Vec>, + shutdown_tx: Option>, + #[cfg(feature = "source")] + dynamic_profile: Arc>>, + + // FFI Bridge handle + bridge_ctx: Option, + // Holding the raw pointer to prevent Sender from being dropped prematurely + _user_data_holder: Option<*mut c_void>, +} + +unsafe impl Send for LibcameraSource {} +unsafe impl Sync for LibcameraSource {} + +impl LibcameraSource { + pub fn from_url(url: &str, config: &crate::config::SourceConfig) -> Result { + let params = super::stream_config_v2::parse_libcamera_url(url)?; + let internal_config = InternalSourceConfig::from_config(config); + + let (rtp_tx, _) = broadcast::channel(1024); + let (state_tx, _) = broadcast::channel(16); + + Ok(Self { + config: internal_config, + params, + state: Arc::new(RwLock::new(StreamSourceState::Initializing)), + rtp_tx, + state_tx, + task_handles: Vec::new(), + shutdown_tx: None, + #[cfg(feature = "source")] + dynamic_profile: Arc::new(RwLock::new(None)), + bridge_ctx: None, + _user_data_holder: None, + }) + } + + async fn set_state(&self, new_state: StreamSourceState, error: Option) { + let mut state = self.state.write().await; + let old_state = *state; + + if old_state != new_state { + *state = new_state; + let _ = self.state_tx.send(StateChangeEvent { + old_state, + new_state, + error: error.clone(), + }); + info!( + "[{}] Libcamera state changed: {:?} -> {:?}{}", + self.config.stream_id, + old_state, + new_state, + error.map(|e| format!(" ({})", e)).unwrap_or_default() + ); + } + } + + /// The C++ callback trampoline (V14.8-SYNC) + unsafe extern "C" fn nal_callback(data: *const u8, size: usize, is_keyframe: c_int, timestamp: u64, user_data: *mut c_void) { + if user_data.is_null() { return; } + + // V14.8: Safety - explicitly scoped unsafe operations + unsafe { + let tx = &*(user_data as *const mpsc::UnboundedSender); + let buf = std::slice::from_raw_parts(data, size); + + let msg = NALMessage { + data: buf.to_vec(), + is_keyframe: is_keyframe != 0, + timestamp_us: timestamp, + }; + + let _ = tx.send(msg); + } + } + + #[cfg(feature = "source")] + async fn build_video_codec_params(&self) -> RTCRtpCodecParameters { + let mime_type = format!("video/{}", self.params.codec.to_uppercase()); + let profile = self.dynamic_profile.read().await.clone().unwrap_or_else(|| self.params.profile.clone()); + + RTCRtpCodecParameters { + capability: RTCRtpCodecCapability { + mime_type, + clock_rate: self.params.clock_rate, + channels: 0, + sdp_fmtp_line: format!("level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}", profile), + rtcp_feedback: vec![ + RTCPFeedback { typ: "goog-remb".into(), parameter: "".into() }, + RTCPFeedback { typ: "nack".into(), parameter: "".into() }, + RTCPFeedback { typ: "nack".into(), parameter: "pli".into() }, + ], + }, + payload_type: self.params.payload_type, + stats_id: String::new(), + } + } +} + +#[async_trait] +impl StreamSource for LibcameraSource { + fn stream_id(&self) -> &str { &self.config.stream_id } + fn state(&self) -> StreamSourceState { *self.state.blocking_read() } + + async fn start(&mut self) -> Result<()> { + if self.bridge_ctx.is_some() { anyhow::bail!("Already started"); } + + let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1); + self.shutdown_tx = Some(shutdown_tx); + + info!("[{}] Starting libcamera-bridge (V14.8-SYNC)", self.config.stream_id); + + let ctx_raw = unsafe { + bridge_init(self.params.width as c_int, self.params.height as c_int, self.params.fps as c_int, self.params.bitrate as c_int, self.params.camera_id as c_int, self.params.rotation as c_int, if self.params.hflip { 1 } else { 0 }, if self.params.vflip { 1 } else { 0 }) + }; + if ctx_raw.is_null() { anyhow::bail!("Failed to initialize libcamera bridge"); } + + let ctx = LibcameraPtr(ctx_raw); + self.bridge_ctx = Some(ctx); + + let (nal_tx, mut nal_rx) = mpsc::unbounded_channel::(); + let nal_tx_box = Box::new(nal_tx); + let user_data_raw = Box::into_raw(nal_tx_box) as *mut c_void; + self._user_data_holder = Some(user_data_raw); + + unsafe { bridge_set_callback(ctx.0, Self::nal_callback, user_data_raw); } + + let _stream_id = self.config.stream_id.clone(); + let rtp_tx = self.rtp_tx.clone(); + let params = self.params.clone(); + #[cfg(feature = "source")] + let dynamic_profile = self.dynamic_profile.clone(); + + self.task_handles.push(tokio::spawn(async move { + let mut packetizer = H264Packetizer::new(1400, params.payload_type, params.clock_rate); + let mut parser = AnnexBParser::new(); + let start_inst = Instant::now(); + let mut last_rtp_ts: u32 = 0; + + loop { + tokio::select! { + _ = shutdown_rx.recv() => break, + msg = nal_rx.recv() => { + if let Some(msg) = msg { + // 1. Sync Timestamp (FFI us -> RTP 90kHz) + let rtp_ts = if msg.timestamp_us > 0 { + (msg.timestamp_us * 9 / 100) as u32 + } else { + (start_inst.elapsed().as_micros() * 9 / 100) as u32 + }; + + // Prevent time going backwards in JitterBuffer + let final_ts = if rtp_ts > last_rtp_ts { rtp_ts } else { last_rtp_ts.wrapping_add(3000) }; + last_rtp_ts = final_ts; + + // 2. Parse AnnexB (Handle multiple NALs in one buffer) + parser.push(&msg.data); + let nals = parser.extract_nals(); + + for nal in nals { + // 3. Profile detection (SPS) + #[cfg(feature = "source")] + if nal.nal_type == super::h264_utils::NalType::Sps { + if let Some(profile) = parse_profile_level_id(&nal.data) { + let mut dp = dynamic_profile.write().await; + if dp.as_ref() != Some(&profile) { + info!("[V14.8] SPS Profile: {}", profile); + *dp = Some(profile); + } + } + } + + // 4. Packetize with real timestamp + packetizer.advance_timestamp(final_ts.wrapping_sub(packetizer.get_current_timestamp())); + + let rtp_packets = packetizer.packetize(&nal); + for packet in rtp_packets { + let _ = rtp_tx.send(MediaPacket::Rtp { + channel: CHANNEL_VIDEO_RTP, + data: packet.to_bytes(), + }); + } + } + } else { break; } + } + } + } + })); + + if unsafe { !bridge_start(ctx.0) } { + let error_msg = unsafe { + let err = bridge_get_error(ctx.0); + if err.is_null() { "Unknown err".to_string() } + else { CStr::from_ptr(err).to_string_lossy().into_owned() } + }; + self.stop().await?; + anyhow::bail!("Bridge failed: {}", error_msg); + } + self.set_state(StreamSourceState::Connected, None).await; + Ok(()) + } + + async fn stop(&mut self) -> Result<()> { + if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } + for h in self.task_handles.drain(..) { let _ = h.await; } + if let Some(ctx) = self.bridge_ctx.take() { unsafe { bridge_stop(ctx.0); bridge_free(ctx.0); } } + if let Some(ptr) = self._user_data_holder.take() { unsafe { let _ = Box::from_raw(ptr as *mut mpsc::UnboundedSender); } } + self.set_state(StreamSourceState::Disconnected, None).await; + Ok(()) + } + + fn subscribe_rtp(&self) -> broadcast::Receiver { self.rtp_tx.subscribe() } + fn subscribe_state(&self) -> broadcast::Receiver { self.state_tx.subscribe() } + + #[cfg(feature = "source")] + async fn get_video_codec(&self) -> Option { Some(self.build_video_codec_params().await) } + #[cfg(feature = "source")] + async fn get_audio_codec(&self) -> Option { None } + + #[cfg(feature = "source")] + async fn get_rtcp_sender(&self) -> Option>> { + let (tx, mut rx) = mpsc::unbounded_channel::>(); + let bridge_ctx = self.bridge_ctx; + tokio::spawn(async move { + while let Some(data) = rx.recv().await { + if let Ok(packets) = webrtc::rtcp::packet::unmarshal(&mut &data[..]) { + for packet in packets { + if packet.as_any().downcast_ref::().is_some() { + if let Some(ctx) = bridge_ctx { unsafe { bridge_request_keyframe(ctx.0) }; } + } + } + } + } + }); + Some(tx) + } +} + +impl Drop for LibcameraSource { + fn drop(&mut self) { + if let Some(ctx) = self.bridge_ctx.take() { unsafe { bridge_free(ctx.0) }; } + if let Some(ptr) = self._user_data_holder.take() { unsafe { let _ = Box::from_raw(ptr as *mut mpsc::UnboundedSender); } } + } +} diff --git a/liveion/src/stream/source/lifecycle.rs b/liveion/src/stream/source/lifecycle.rs new file mode 100644 index 00000000..f7a00c6e --- /dev/null +++ b/liveion/src/stream/source/lifecycle.rs @@ -0,0 +1,354 @@ +//! Stream lifecycle state machine +//! +//! Tracks the full lifecycle of a stream from creation to active to failed. +//! This is complementary to the existing `StreamSourceState` which tracks +//! the connection-level state of a source. +//! +//! State transitions: +//! ```text +//! Created ──(source connected)──→ Connected ──(first data)──→ Active +//! ↑ │ │ +//! │ └──(error)──→ Failed ──────┘ +//! └────────────────────────────────────────────── (reset) ─────┘ +//! ``` + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The high-level lifecycle state of a stream. +/// +/// This is distinct from `StreamSourceState` which tracks the low-level +/// connection status. `StreamLifecycleState` represents the logical +/// readiness of the stream for consumption. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum StreamLifecycleState { + /// Empty stream (placeholder). No codec parameters known. + /// Can be used for pre-authorization or reservation. + Created, + + /// Source connected (WebRTC/RTP established) but no media data received yet. + Connected, + + /// Actively receiving and forwarding media data. + Active, + + /// Error state. Retains codec parameter information from previous active state. + Failed, +} + +impl Default for StreamLifecycleState { + fn default() -> Self { + Self::Created + } +} + +impl fmt::Display for StreamLifecycleState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Created => write!(f, "created"), + Self::Connected => write!(f, "connected"), + Self::Active => write!(f, "active"), + Self::Failed => write!(f, "failed"), + } + } +} + +/// Policy controlling when a source daemon should run. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DaemonPolicy { + /// Start when there are subscribers, stop when there are none. + Auto, + + /// Always keep the source running regardless of subscriber count. + Always, +} + +impl Default for DaemonPolicy { + fn default() -> Self { + Self::Always + } +} + +impl fmt::Display for DaemonPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Auto => write!(f, "auto"), + Self::Always => write!(f, "always"), + } + } +} + +/// Retained codec information from a previously active stream. +/// Used when transitioning to `Failed` state so that the stream +/// can be recreated with the same parameters. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CodecInfo { + pub video_mime_type: Option, + pub video_clock_rate: Option, + pub video_sdp_fmtp: Option, + pub audio_mime_type: Option, + pub audio_clock_rate: Option, +} + +/// A lifecycle event emitted when the stream state changes. +#[derive(Debug, Clone, Serialize)] +pub struct LifecycleEvent { + pub stream_id: String, + pub old_state: StreamLifecycleState, + pub new_state: StreamLifecycleState, + pub timestamp: DateTime, + pub error: Option, +} + +/// Manages the lifecycle state of a single stream. +pub struct StreamLifecycle { + stream_id: String, + state: StreamLifecycleState, + daemon_policy: DaemonPolicy, + subscriber_count: usize, + last_error: Option, + codec_info: Option, + created_at: DateTime, + last_state_change: DateTime, +} + +impl StreamLifecycle { + /// Create a new lifecycle tracker for a stream. + pub fn new(stream_id: String, daemon_policy: DaemonPolicy) -> Self { + let now = Utc::now(); + Self { + stream_id, + state: StreamLifecycleState::Created, + daemon_policy, + subscriber_count: 0, + last_error: None, + codec_info: None, + created_at: now, + last_state_change: now, + } + } + + /// Get the current lifecycle state. + pub fn state(&self) -> StreamLifecycleState { + self.state + } + + /// Get the stream ID. + pub fn stream_id(&self) -> &str { + &self.stream_id + } + + /// Get the daemon policy. + pub fn daemon_policy(&self) -> DaemonPolicy { + self.daemon_policy + } + + /// Get the retained codec info (available in Failed state). + pub fn codec_info(&self) -> Option<&CodecInfo> { + self.codec_info.as_ref() + } + + /// Get the last error message. + pub fn last_error(&self) -> Option<&str> { + self.last_error.as_deref() + } + + /// Get the creation timestamp. + pub fn created_at(&self) -> DateTime { + self.created_at + } + + /// Get the timestamp of the last state change. + pub fn last_state_change(&self) -> DateTime { + self.last_state_change + } + + /// Get the current subscriber count. + pub fn subscriber_count(&self) -> usize { + self.subscriber_count + } + + /// Transition to Connected state. + /// Valid from: Created, Failed + pub fn on_connected(&mut self) -> Option { + match self.state { + StreamLifecycleState::Created | StreamLifecycleState::Failed => { + let old = self.state; + self.state = StreamLifecycleState::Connected; + self.last_error = None; + self.last_state_change = Utc::now(); + Some(LifecycleEvent { + stream_id: self.stream_id.clone(), + old_state: old, + new_state: self.state, + timestamp: self.last_state_change, + error: None, + }) + } + _ => None, + } + } + + /// Transition to Active state. + /// Valid from: Connected + pub fn on_active(&mut self) -> Option { + if self.state == StreamLifecycleState::Connected { + let old = self.state; + self.state = StreamLifecycleState::Active; + self.last_state_change = Utc::now(); + Some(LifecycleEvent { + stream_id: self.stream_id.clone(), + old_state: old, + new_state: self.state, + timestamp: self.last_state_change, + error: None, + }) + } else { + None + } + } + + /// Transition to Failed state, retaining codec info. + /// Valid from: Connected, Active + pub fn on_failed(&mut self, error: String, codec_info: Option) -> Option { + match self.state { + StreamLifecycleState::Connected | StreamLifecycleState::Active => { + let old = self.state; + self.state = StreamLifecycleState::Failed; + self.last_error = Some(error.clone()); + if codec_info.is_some() { + self.codec_info = codec_info; + } + self.last_state_change = Utc::now(); + Some(LifecycleEvent { + stream_id: self.stream_id.clone(), + old_state: old, + new_state: self.state, + timestamp: self.last_state_change, + error: Some(error), + }) + } + _ => None, + } + } + + /// Reset to Created state. + pub fn reset(&mut self) -> Option { + let old = self.state; + self.state = StreamLifecycleState::Created; + self.last_error = None; + self.codec_info = None; + self.last_state_change = Utc::now(); + Some(LifecycleEvent { + stream_id: self.stream_id.clone(), + old_state: old, + new_state: self.state, + timestamp: self.last_state_change, + error: None, + }) + } + + /// Update the subscriber count. Returns true if the source should be + /// started/stopped based on daemon policy. + pub fn update_subscribers(&mut self, count: usize) -> bool { + let old_count = self.subscriber_count; + self.subscriber_count = count; + + match self.daemon_policy { + DaemonPolicy::Always => false, // Never stop + DaemonPolicy::Auto => { + // Signal change when crossing the zero boundary + (old_count == 0 && count > 0) || (old_count > 0 && count == 0) + } + } + } + + /// Whether the source should be running based on daemon policy and subscriber count. + pub fn should_source_run(&self) -> bool { + match self.daemon_policy { + DaemonPolicy::Always => true, + DaemonPolicy::Auto => self.subscriber_count > 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lifecycle_transitions() { + let mut lc = StreamLifecycle::new("test-stream".to_string(), DaemonPolicy::Always); + assert_eq!(lc.state(), StreamLifecycleState::Created); + + // Created -> Connected + let event = lc.on_connected(); + assert!(event.is_some()); + assert_eq!(lc.state(), StreamLifecycleState::Connected); + + // Connected -> Active + let event = lc.on_active(); + assert!(event.is_some()); + assert_eq!(lc.state(), StreamLifecycleState::Active); + + // Active -> Failed + let event = lc.on_failed("test error".to_string(), Some(CodecInfo { + video_mime_type: Some("video/H264".to_string()), + video_clock_rate: Some(90000), + video_sdp_fmtp: None, + audio_mime_type: None, + audio_clock_rate: None, + })); + assert!(event.is_some()); + assert_eq!(lc.state(), StreamLifecycleState::Failed); + assert!(lc.codec_info().is_some()); + assert_eq!(lc.last_error(), Some("test error")); + + // Failed -> Connected (retry) + let event = lc.on_connected(); + assert!(event.is_some()); + assert_eq!(lc.state(), StreamLifecycleState::Connected); + } + + #[test] + fn test_invalid_transitions() { + let mut lc = StreamLifecycle::new("test".to_string(), DaemonPolicy::Always); + + // Created -> Active (invalid, must go through Connected) + let event = lc.on_active(); + assert!(event.is_none()); + assert_eq!(lc.state(), StreamLifecycleState::Created); + } + + #[test] + fn test_daemon_policy_auto() { + let mut lc = StreamLifecycle::new("test".to_string(), DaemonPolicy::Auto); + + assert!(!lc.should_source_run()); + + let changed = lc.update_subscribers(1); + assert!(changed); + assert!(lc.should_source_run()); + + let changed = lc.update_subscribers(2); + assert!(!changed); // No boundary crossing + assert!(lc.should_source_run()); + + let changed = lc.update_subscribers(0); + assert!(changed); + assert!(!lc.should_source_run()); + } + + #[test] + fn test_daemon_policy_always() { + let mut lc = StreamLifecycle::new("test".to_string(), DaemonPolicy::Always); + + assert!(lc.should_source_run()); + + let changed = lc.update_subscribers(0); + assert!(!changed); // Always policy never signals + assert!(lc.should_source_run()); + } +} diff --git a/liveion/src/stream/source/mod.rs b/liveion/src/stream/source/mod.rs index 5cc9e15a..230be0d3 100644 --- a/liveion/src/stream/source/mod.rs +++ b/liveion/src/stream/source/mod.rs @@ -1,18 +1,35 @@ use anyhow::Result; use async_trait::async_trait; use tokio::sync::broadcast; +use bytes::Bytes; #[cfg(feature = "source-rtsp")] mod rtsp_source; #[cfg(feature = "source-sdp")] mod sdp_source; +pub mod lifecycle; +pub mod stream_config_v2; +pub mod h264_utils; +pub mod source_router; + +#[cfg(feature = "source-rtp")] +pub mod rtp_listener; +#[cfg(feature = "source-libcamera")] +pub mod libcamera_source; +#[cfg(feature = "source-libcamera")] +pub mod v4l2_source; + pub mod manager; #[cfg(feature = "source-rtsp")] pub use rtsp_source::RtspSource; #[cfg(feature = "source-sdp")] pub use sdp_source::SdpSource; +#[cfg(feature = "source-libcamera")] +pub use libcamera_source::LibcameraSource; +#[cfg(feature = "source-libcamera")] +pub use v4l2_source::V4L2Source; pub use manager::SourceManager; @@ -34,7 +51,7 @@ pub struct StateChangeEvent { #[derive(Debug, Clone)] pub enum MediaPacket { - Rtp { channel: u8, data: Vec }, + Rtp { channel: u8, data: Bytes }, } #[derive(Debug, Clone)] @@ -102,7 +119,14 @@ pub async fn create_source_from_url( url: &str, config: &crate::config::SourceConfig, ) -> Result> { - let internal_config = InternalSourceConfig::from_config(config); + source_router::create_source_extended(url, config).await +} + +pub(crate) async fn create_legacy_source_from_url( + url: &str, + config: &crate::config::SourceConfig, +) -> Result> { + let _internal_config = InternalSourceConfig::from_config(config); if url.starts_with("rtsp://") || url.starts_with("rtsps://") { #[cfg(feature = "source-rtsp")] diff --git a/liveion/src/stream/source/rtp_listener.rs b/liveion/src/stream/source/rtp_listener.rs new file mode 100644 index 00000000..c3bc7ce5 --- /dev/null +++ b/liveion/src/stream/source/rtp_listener.rs @@ -0,0 +1,381 @@ +//! RTP Listener Source +//! +//! Binds to a UDP port and listens for incoming RTP packets. +//! Implements the `StreamSource` trait. +//! +//! URL format: `rtp://0.0.0.0:5004?codec=H264&profile=42001f` +//! +//! This is functionally similar to `SdpSource` but configured via URL +//! rather than an SDP file. It always listens on a single port for +//! a single video stream. + +use super::{InternalSourceConfig, MediaPacket, StateChangeEvent, StreamSource, StreamSourceState}; +use super::stream_config_v2::parse_rtp_url; +use anyhow::Result; +use async_trait::async_trait; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use tokio::sync::{RwLock, broadcast}; +use tracing::{debug, error, info, trace}; + +#[cfg(feature = "source")] +use tokio::sync::mpsc; + +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::RTCPFeedback; + +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters}; + +/// Channel constants matching the SourceBridge convention +const CHANNEL_VIDEO_RTP: u8 = 0; + +/// Configuration parsed from the rtp:// URL +#[derive(Debug, Clone)] +struct RtpConfig { + bind_addr: SocketAddr, + codec: String, + profile: String, + clock_rate: u32, + payload_type: u8, +} + +pub struct RtpListenerSource { + config: InternalSourceConfig, + rtp_config: RtpConfig, + state: Arc>, + rtp_tx: broadcast::Sender, + state_tx: broadcast::Sender, + task_handles: Vec>, + shutdown_tx: Option>, + #[cfg(feature = "source")] + rtcp_tx: Arc)>>>>, +} + +impl RtpListenerSource { + /// Create a new RTP listener source from a URL. + /// + /// URL format: `rtp://host:port?codec=H264&profile=42001f` + pub fn from_url(url: &str, config: &crate::config::SourceConfig) -> Result { + let params = parse_rtp_url(url)?; + let internal_config = InternalSourceConfig::from_config(config); + + let rtp_config = RtpConfig { + bind_addr: params.bind_addr, + codec: params.codec, + profile: params.profile, + clock_rate: params.clock_rate, + payload_type: params.payload_type, + }; + + let (rtp_tx, _) = broadcast::channel(1024); + let (state_tx, _) = broadcast::channel(16); + + Ok(Self { + config: internal_config, + rtp_config, + state: Arc::new(RwLock::new(StreamSourceState::Initializing)), + rtp_tx, + state_tx, + task_handles: Vec::new(), + shutdown_tx: None, + #[cfg(feature = "source")] + rtcp_tx: Arc::new(RwLock::new(None)), + }) + } + + async fn set_state(&self, new_state: StreamSourceState, error: Option) { + let mut state = self.state.write().await; + let old_state = *state; + + if old_state != new_state { + *state = new_state; + let _ = self.state_tx.send(StateChangeEvent { + old_state, + new_state, + error: error.clone(), + }); + info!( + "[{}] State changed: {:?} -> {:?}{}", + self.config.stream_id, + old_state, + new_state, + error.map(|e| format!(" ({})", e)).unwrap_or_default() + ); + } + } + + /// Main UDP receive loop + async fn udp_receive_loop( + stream_id: String, + bind_addr: SocketAddr, + rtp_tx: broadcast::Sender, + state: Arc>, + state_tx: broadcast::Sender, + mut shutdown_rx: broadcast::Receiver<()>, + ) { + info!("[{}] Starting RTP listener on {}", stream_id, bind_addr); + + let socket = match UdpSocket::bind(bind_addr).await { + Ok(s) => { + info!("[{}] UDP socket bound to {}", stream_id, bind_addr); + s + } + Err(e) => { + error!( + "[{}] Failed to bind UDP socket on {}: {}", + stream_id, bind_addr, e + ); + let mut s = state.write().await; + *s = StreamSourceState::Error; + let _ = state_tx.send(StateChangeEvent { + old_state: StreamSourceState::Initializing, + new_state: StreamSourceState::Error, + error: Some(format!("bind failed: {}", e)), + }); + return; + } + }; + + // Transition to Connected + { + let mut s = state.write().await; + let old = *s; + *s = StreamSourceState::Connected; + let _ = state_tx.send(StateChangeEvent { + old_state: old, + new_state: StreamSourceState::Connected, + error: None, + }); + } + + let mut buf = vec![0u8; 2048]; + let mut packet_count: u64 = 0; + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + info!( + "[{}] RTP listener shutting down (received {} packets)", + stream_id, packet_count + ); + break; + } + result = socket.recv_from(&mut buf) => { + match result { + Ok((len, _addr)) => { + packet_count += 1; + + let packet = MediaPacket::Rtp { + channel: CHANNEL_VIDEO_RTP, + data: buf[..len].to_vec().into(), + }; + + if rtp_tx.send(packet).is_err() { + // No subscribers, that's ok + } + + if packet_count % 1000 == 0 { + trace!( + "[{}] RTP listener: received {} packets", + stream_id, packet_count + ); + } + + if packet_count == 1 { + debug!( + "[{}] First RTP packet received ({} bytes)", + stream_id, len + ); + } + } + Err(e) => { + error!("[{}] UDP receive error: {}", stream_id, e); + } + } + } + } + } + } + + #[cfg(feature = "source")] + fn build_video_codec_params(&self) -> RTCRtpCodecParameters { + let mime_type = format!("video/{}", self.rtp_config.codec.to_uppercase()); + + let sdp_fmtp_line = if self.rtp_config.codec.to_uppercase() == "H264" { + format!( + "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}", + self.rtp_config.profile + ) + } else { + String::new() + }; + + RTCRtpCodecParameters { + capability: RTCRtpCodecCapability { + mime_type, + clock_rate: self.rtp_config.clock_rate, + channels: 0, + sdp_fmtp_line, + rtcp_feedback: vec![ + RTCPFeedback { + typ: "goog-remb".to_owned(), + parameter: "".to_owned(), + }, + RTCPFeedback { + typ: "ccm".to_owned(), + parameter: "fir".to_owned(), + }, + RTCPFeedback { + typ: "nack".to_owned(), + parameter: "".to_owned(), + }, + RTCPFeedback { + typ: "nack".to_owned(), + parameter: "pli".to_owned(), + }, + ], + }, + payload_type: self.rtp_config.payload_type, + stats_id: String::new(), + } + } +} + +#[async_trait] +impl StreamSource for RtpListenerSource { + fn stream_id(&self) -> &str { + &self.config.stream_id + } + + fn state(&self) -> StreamSourceState { + *self.state.blocking_read() + } + + async fn start(&mut self) -> Result<()> { + if !self.task_handles.is_empty() { + anyhow::bail!("Source already started"); + } + + let (shutdown_tx, _) = broadcast::channel(1); + self.shutdown_tx = Some(shutdown_tx.clone()); + + // RTCP sender task (for forwarding RTCP from WebRTC subscribers back to RTP source) + #[cfg(feature = "source")] + { + let (rtcp_internal_tx, rtcp_rx) = mpsc::unbounded_channel(); + let mut rtcp_store = self.rtcp_tx.write().await; + *rtcp_store = Some(rtcp_internal_tx); + + let stream_id = self.config.stream_id.clone(); + let shutdown_rx = shutdown_tx.subscribe(); + + let rtcp_task = tokio::spawn(async move { + Self::rtcp_sender_task(stream_id, rtcp_rx, shutdown_rx).await; + }); + self.task_handles.push(rtcp_task); + } + + // Main UDP receive task + let stream_id = self.config.stream_id.clone(); + let bind_addr = self.rtp_config.bind_addr; + let rtp_tx = self.rtp_tx.clone(); + let state = self.state.clone(); + let state_tx = self.state_tx.clone(); + let shutdown_rx = shutdown_tx.subscribe(); + + let recv_task = tokio::spawn(async move { + Self::udp_receive_loop(stream_id, bind_addr, rtp_tx, state, state_tx, shutdown_rx) + .await; + }); + self.task_handles.push(recv_task); + + info!( + "[{}] RTP listener source started on {}", + self.config.stream_id, self.rtp_config.bind_addr + ); + + Ok(()) + } + + async fn stop(&mut self) -> Result<()> { + if let Some(tx) = self.shutdown_tx.take() { + let _ = tx.send(()); + } + + for handle in self.task_handles.drain(..) { + let _ = handle.await; + } + + self.set_state(StreamSourceState::Disconnected, None).await; + + info!("[{}] RTP listener source stopped", self.config.stream_id); + Ok(()) + } + + fn subscribe_rtp(&self) -> broadcast::Receiver { + self.rtp_tx.subscribe() + } + + fn subscribe_state(&self) -> broadcast::Receiver { + self.state_tx.subscribe() + } + + #[cfg(feature = "source")] + async fn get_video_codec(&self) -> Option { + // Codec is known from URL configuration, always available + Some(self.build_video_codec_params()) + } + + #[cfg(feature = "source")] + async fn get_audio_codec(&self) -> Option { + // RTP listener currently only supports video + None + } + + #[cfg(feature = "source")] + async fn get_rtcp_sender(&self) -> Option>> { + // For RTP listener, we don't have a specific RTCP target address + // (the source could be anywhere). Return None for now. + // TODO: Track the source address from received packets and send RTCP back. + None + } +} + +#[cfg(feature = "source")] +impl RtpListenerSource { + async fn rtcp_sender_task( + stream_id: String, + mut rtcp_rx: mpsc::UnboundedReceiver<(SocketAddr, Vec)>, + mut shutdown_rx: broadcast::Receiver<()>, + ) { + info!("[{}] RTCP sender task started", stream_id); + + let socket = match UdpSocket::bind("0.0.0.0:0").await { + Ok(s) => s, + Err(e) => { + error!("[{}] Failed to create RTCP socket: {}", stream_id, e); + return; + } + }; + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + info!("[{}] RTCP sender task shutting down", stream_id); + break; + } + Some((addr, data)) = rtcp_rx.recv() => { + debug!( + "[{}] Sending RTCP to {}, size: {} bytes", + stream_id, addr, data.len() + ); + if let Err(e) = socket.send_to(&data, addr).await { + error!("[{}] Failed to send RTCP: {}", stream_id, e); + } + } + } + } + } +} diff --git a/liveion/src/stream/source/rtsp_source.rs b/liveion/src/stream/source/rtsp_source.rs index 03953e2a..6707a11a 100644 --- a/liveion/src/stream/source/rtsp_source.rs +++ b/liveion/src/stream/source/rtsp_source.rs @@ -367,7 +367,7 @@ impl RtspSource { let packet = MediaPacket::Rtp { channel, - data, + data: data.into(), }; if rtp_tx.send(packet).is_err() { diff --git a/liveion/src/stream/source/sdp_source.rs b/liveion/src/stream/source/sdp_source.rs index a2424940..cf27e1f3 100644 --- a/liveion/src/stream/source/sdp_source.rs +++ b/liveion/src/stream/source/sdp_source.rs @@ -406,7 +406,7 @@ impl SdpSource { let packet = MediaPacket::Rtp { channel: ctx.channel, - data: buf[..len].to_vec(), + data: buf[..len].to_vec().into(), }; if ctx.rtp_tx.send(packet).is_err() { diff --git a/liveion/src/stream/source/source_router.rs b/liveion/src/stream/source/source_router.rs new file mode 100644 index 00000000..ac6e67d2 --- /dev/null +++ b/liveion/src/stream/source/source_router.rs @@ -0,0 +1,76 @@ +//! Extended Source Router +//! +//! Wraps the existing `create_source_from_url` and adds support for +//! the new v2 URL schemes (`rtp://` and `exec://`). +//! +//! This acts as an adapter layer to maintain backward compatibility +//! while adding new source types incrementally. + +use super::StreamSource; +use anyhow::Result; + +#[cfg(feature = "source")] +use crate::config::SourceConfig; + +#[cfg(feature = "source-rtp")] +use super::rtp_listener::RtpListenerSource; + +#[cfg(feature = "source-libcamera")] +use super::libcamera_source::LibcameraSource; + +/// Creates a `StreamSource` from a connection URL. +/// +/// Intercepts new URL schemes (rtp://, exec://) and delegates +/// everything else to the existing `create_source_from_url` function. +#[cfg(feature = "source")] +pub async fn create_source_extended(url: &str, config: &SourceConfig) -> Result> { + let url_lower = url.to_lowercase(); + + // Check for RTP Listener scheme + if url_lower.starts_with("rtp://") { + #[cfg(feature = "source-rtp")] + { + let source = RtpListenerSource::from_url(url, config)?; + return Ok(Box::new(source)); + } + #[cfg(not(feature = "source-rtp"))] + { + anyhow::bail!("RTP source feature not enabled. Recompile with feature 'source-rtp'"); + } + } + + // Check for Libcamera-Bridge scheme + if url_lower.starts_with("libcamera://") { + #[cfg(feature = "source-libcamera")] + { + let source = LibcameraSource::from_url(url, config)?; + return Ok(Box::new(source)); + } + #[cfg(not(feature = "source-libcamera"))] + { + anyhow::bail!("Libcamera source feature not enabled. Recompile with feature 'source-libcamera'"); + } + } + + // Check for V4L2 Direct Capture scheme + if url_lower.starts_with("v4l2://") { + #[cfg(feature = "source-libcamera")] + { + use super::v4l2_source::V4L2Source; + let source = V4L2Source::from_url(url, config)?; + return Ok(Box::new(source)); + } + #[cfg(not(feature = "source-libcamera"))] + { + anyhow::bail!("V4L2 source requires feature 'source-libcamera' (shared hardware encoder)"); + } + } + + // Delegate to existing, unmodified factory for legacy schemes (rtsp://, file://, .sdp) + super::create_legacy_source_from_url(url, config).await +} + +#[cfg(not(feature = "source"))] +pub async fn create_source_extended(_url: &str, _config: &crate::config::SourceConfig) -> Result> { + anyhow::bail!("Source feature not enabled") +} diff --git a/liveion/src/stream/source/stream_config_v2.rs b/liveion/src/stream/source/stream_config_v2.rs new file mode 100644 index 00000000..bb5966ba --- /dev/null +++ b/liveion/src/stream/source/stream_config_v2.rs @@ -0,0 +1,434 @@ +//! Stream configuration v2 +//! +//! New configuration format that coexists with the existing `SourceConfig`. +//! Supports URL-based source specification, daemon policies, and recording policies. +//! +//! Example TOML: +//! ```toml +//! [streams.my_camera] +//! source = "rtp://0.0.0.0:5004" +//! daemon = "always" +//! +//! [streams.my_camera] +//! source = "rtp://0.0.0.0:5004" +//! daemon = "always" +//! +//! [streams.remote_feed] +//! source = "rtsp://admin:pass@192.168.1.100:554/stream1" +//! daemon = "auto" +//! record = "always" +//! ``` + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::lifecycle::DaemonPolicy; + +/// Configuration for a single stream (v2 format). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StreamEntryConfig { + /// Source URL. Supported schemes: + /// - `rtp://host:port` — Listen for incoming RTP on a UDP port + /// - `rtp://host:port` — Listen for incoming RTP on a UDP port + /// - `rtsp://user:pass@host:port/path` — RTSP client pull + /// - `file:///path/to/file.sdp` or `/path/to.sdp` — SDP file source + pub source: String, + + /// Daemon policy: when to keep the source running. + /// - `"always"` (default): source runs regardless of subscribers + /// - `"auto"`: source starts when subscribers arrive, stops when they leave + #[serde(default = "default_daemon")] + pub daemon: String, + + /// Recording policy (optional). + /// - `"auto"`: record when the stream is active + /// - `"always"`: always record + /// - `null` / absent: no recording + #[serde(default)] + pub record: Option, +} + +fn default_daemon() -> String { + "always".to_string() +} + +impl StreamEntryConfig { + /// Parse the daemon policy from the string configuration. + pub fn daemon_policy(&self) -> DaemonPolicy { + match self.daemon.to_lowercase().as_str() { + "auto" => DaemonPolicy::Auto, + _ => DaemonPolicy::Always, + } + } + + /// Validate the stream configuration. + pub fn validate(&self) -> anyhow::Result<()> { + if self.source.trim().is_empty() { + anyhow::bail!("source URL cannot be empty"); + } + + let src = self.source.to_lowercase(); + let valid = src.starts_with("rtp://") + || src.starts_with("libcamera://") + || src.starts_with("v4l2://") + || src.starts_with("rtsp://") + || src.starts_with("rtsps://") + || src.starts_with("file://") + || src.ends_with(".sdp"); + + if !valid { + anyhow::bail!( + "Unsupported source URL: {}. Valid schemes: rtp://, libcamera://, v4l2://, rtsp://, file://, .sdp", + self.source + ); + } + + // Validate daemon value + match self.daemon.to_lowercase().as_str() { + "auto" | "always" => {} + other => anyhow::bail!( + "Invalid daemon value: '{}'. Must be 'auto' or 'always'", + other + ), + } + + // Validate record value + if let Some(ref record) = self.record { + match record.to_lowercase().as_str() { + "auto" | "always" => {} + other => anyhow::bail!( + "Invalid record value: '{}'. Must be 'auto' or 'always'", + other + ), + } + } + + Ok(()) + } + + /// Convert the v2 config to the existing SourceConfig format for backward compatibility. + pub fn to_legacy_source_config(&self, stream_id: &str) -> crate::config::SourceConfig { + crate::config::SourceConfig { + stream_id: stream_id.to_string(), + url: self.source.clone(), + } + } +} + +/// Top-level streams configuration (v2 format). +/// +/// This sits alongside the existing `StreamConfig` in the config file. +/// Both formats can coexist in the same TOML file. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct StreamsConfigV2 { + /// Map of stream_id -> stream configuration + #[serde(default)] + pub streams: HashMap, +} + +impl StreamsConfigV2 { + /// Validate all stream configurations. + pub fn validate(&self) -> anyhow::Result<()> { + for (stream_id, config) in &self.streams { + config + .validate() + .map_err(|e| anyhow::anyhow!("Stream '{}': {}", stream_id, e))?; + } + Ok(()) + } + + /// Convert all v2 configs to legacy SourceConfig format. + pub fn to_legacy_configs(&self) -> Vec { + self.streams + .iter() + .map(|(id, cfg)| cfg.to_legacy_source_config(id)) + .collect() + } +} + + +/// Parse the `libcamera://` URL. +/// +/// Supports query parameters for width, height, fps, bitrate, etc. +/// Format: `libcamera:///path/to/bin?width=640&height=480&fps=30&bitrate=2000000` +pub fn parse_libcamera_url(url: &str) -> anyhow::Result { + let stripped = url.strip_prefix("libcamera://").ok_or_else(|| anyhow::anyhow!("URL must start with libcamera://"))?; + let (addr_part, query_part) = match stripped.find('?') { + Some(idx) => (&stripped[..idx], Some(&stripped[idx + 1..])), + None => (stripped, None), + }; + + let mut width = 1280; + let mut height = 720; + let mut fps = 30; + let mut bitrate = 2_000_000; + let mut camera_id = 0; + let mut rotation = 0; + let mut hflip = false; + let mut vflip = false; + let mut codec = "H264".to_string(); + let mut profile = "42001f".to_string(); + let clock_rate = 90000; + let mut payload_type = 96; + + if let Some(query) = query_part { + for param in query.split('&') { + let (key, value) = match param.find('=') { + Some(idx) => (¶m[..idx], ¶m[idx + 1..]), + None => continue, + }; + + match key { + "width" | "w" => width = value.parse().unwrap_or(1280), + "height" | "h" => height = value.parse().unwrap_or(720), + "fps" | "f" => fps = value.parse().unwrap_or(30), + "bitrate" | "b" => bitrate = value.parse().unwrap_or(2_000_000), + "camera" | "c" => camera_id = value.parse().unwrap_or(0), + "rotation" | "r" => rotation = value.parse().unwrap_or(0), + "hflip" => hflip = value == "true" || value == "1", + "vflip" => vflip = value == "true" || value == "1", + "codec" => codec = value.to_uppercase(), + "profile" => profile = value.into(), + "pt" => payload_type = value.parse().unwrap_or(96), + _ => {} + } + } + } + + Ok(LibcameraUrlParams { + placeholder_path: addr_part.to_string(), + width, + height, + fps, + bitrate, + camera_id, + rotation, + hflip, + vflip, + codec, + profile, + clock_rate, + payload_type, + }) +} + +/// Parsed parameters from a `libcamera://` URL. +#[derive(Debug, Clone)] +pub struct LibcameraUrlParams { + pub placeholder_path: String, + pub width: u32, + pub height: u32, + pub fps: u32, + pub bitrate: u32, + pub camera_id: u32, + pub rotation: u32, + pub hflip: bool, + pub vflip: bool, + pub codec: String, + pub profile: String, + pub clock_rate: u32, + pub payload_type: u8, +} + +/// Parsed parameters from an `exec://` URL. +#[derive(Debug, Clone)] +pub struct ExecUrlParams { + pub executable: String, + pub args: Vec, + pub codec: String, + pub profile: String, + pub clock_rate: u32, + pub payload_type: u8, +} + +/// Parse a `rtp://` URL to extract bind address. +/// +/// Format: `rtp://host:port` +/// +/// Query parameters: +/// - `codec`: video codec name (default: "H264") +/// - `profile`: H.264 profile-level-id (default: "42001f") +/// - `clock_rate`: RTP clock rate (default: 90000) +/// - `payload_type`: RTP payload type (default: 96) +pub fn parse_rtp_url(url: &str) -> anyhow::Result { + let stripped = url + .strip_prefix("rtp://") + .ok_or_else(|| anyhow::anyhow!("URL must start with rtp://"))?; + + let (addr_part, query_part) = match stripped.find('?') { + Some(idx) => (&stripped[..idx], Some(&stripped[idx + 1..])), + None => (stripped, None), + }; + + let bind_addr: std::net::SocketAddr = addr_part + .parse() + .map_err(|e| anyhow::anyhow!("Invalid bind address '{}': {}", addr_part, e))?; + + let mut codec = "H264".to_string(); + let mut profile = "42001f".to_string(); + let mut clock_rate: u32 = 90000; + let mut payload_type: u8 = 96; + + if let Some(query) = query_part { + for param in query.split('&') { + let (key, value) = match param.find('=') { + Some(idx) => (¶m[..idx], ¶m[idx + 1..]), + None => continue, + }; + + match key { + "codec" => codec = value.to_uppercase(), + "profile" => profile = value.to_string(), + "clock_rate" => clock_rate = value.parse().unwrap_or(90000), + "payload_type" | "pt" => payload_type = value.parse().unwrap_or(96), + _ => { + tracing::warn!("Unknown rtp:// query parameter: {}={}", key, value); + } + } + } + } + + Ok(RtpUrlParams { + bind_addr, + codec, + profile, + clock_rate, + payload_type, + }) +} + +/// Parsed parameters from an `rtp://` URL. +#[derive(Debug, Clone)] +pub struct RtpUrlParams { + pub bind_addr: std::net::SocketAddr, + pub codec: String, + pub profile: String, + pub clock_rate: u32, + pub payload_type: u8, +} + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_parse_rtp_url() { + let params = parse_rtp_url("rtp://0.0.0.0:5004").unwrap(); + assert_eq!(params.bind_addr.port(), 5004); + assert_eq!(params.codec, "H264"); + assert_eq!(params.payload_type, 96); + } + + #[test] + fn test_parse_rtp_url_with_query() { + let params = + parse_rtp_url("rtp://0.0.0.0:5004?codec=H265&pt=97&clock_rate=90000").unwrap(); + assert_eq!(params.codec, "H265"); + assert_eq!(params.payload_type, 97); + } + + #[test] + fn test_stream_entry_config_validate() { + let cfg = StreamEntryConfig { + source: "rtp://0.0.0.0:5004".to_string(), + daemon: "always".to_string(), + record: None, + }; + assert!(cfg.validate().is_ok()); + + let cfg = StreamEntryConfig { + source: "".to_string(), + daemon: "always".to_string(), + record: None, + }; + assert!(cfg.validate().is_err()); + + let cfg = StreamEntryConfig { + source: "rtp://0.0.0.0:5004".to_string(), + daemon: "invalid".to_string(), + record: None, + }; + assert!(cfg.validate().is_err()); + } + + #[test] + fn test_daemon_policy_parsing() { + let cfg = StreamEntryConfig { + source: "rtp://0.0.0.0:5004".to_string(), + daemon: "auto".to_string(), + record: None, + }; + assert_eq!(cfg.daemon_policy(), DaemonPolicy::Auto); + + let cfg = StreamEntryConfig { + source: "rtp://0.0.0.0:5004".to_string(), + daemon: "always".to_string(), + record: None, + }; + assert_eq!(cfg.daemon_policy(), DaemonPolicy::Always); + } +} + +/// Parse a `v4l2://` URL for direct V4L2 capture. +/// +/// Format: `v4l2:///dev/video2?width=640&height=480&fps=30&bitrate=2000000` +pub fn parse_v4l2_url(url: &str) -> anyhow::Result { + let stripped = url.strip_prefix("v4l2://").ok_or_else(|| anyhow::anyhow!("URL must start with v4l2://"))?; + let (device_part, query_part) = match stripped.find('?') { + Some(idx) => (&stripped[..idx], Some(&stripped[idx + 1..])), + None => (stripped, None), + }; + + let device = if device_part.is_empty() { "/dev/video2".to_string() } else { device_part.to_string() }; + + let mut width: u32 = 640; + let mut height: u32 = 480; + let mut fps: u32 = 30; + let mut bitrate: u32 = 2_000_000; + let mut profile = "42001f".to_string(); + let clock_rate: u32 = 90000; + let mut payload_type: u8 = 96; + + if let Some(query) = query_part { + for param in query.split('&') { + let (key, value) = match param.find('=') { + Some(idx) => (¶m[..idx], ¶m[idx + 1..]), + None => continue, + }; + match key { + "width" | "w" => width = value.parse().unwrap_or(640), + "height" | "h" => height = value.parse().unwrap_or(480), + "fps" | "f" => fps = value.parse().unwrap_or(30), + "bitrate" | "b" => bitrate = value.parse().unwrap_or(2_000_000), + "profile" => profile = value.into(), + "pt" => payload_type = value.parse().unwrap_or(96), + _ => {} + } + } + } + + Ok(V4L2UrlParams { + device, + width, + height, + fps, + bitrate, + profile, + clock_rate, + payload_type, + }) +} + +/// Parsed parameters from a `v4l2://` URL. +#[derive(Debug, Clone)] +pub struct V4L2UrlParams { + pub device: String, + pub width: u32, + pub height: u32, + pub fps: u32, + pub bitrate: u32, + pub profile: String, + pub clock_rate: u32, + pub payload_type: u8, +} diff --git a/liveion/src/stream/source/v4l2_source.rs b/liveion/src/stream/source/v4l2_source.rs new file mode 100644 index 00000000..5d744d40 --- /dev/null +++ b/liveion/src/stream/source/v4l2_source.rs @@ -0,0 +1,339 @@ +//! V4L2 Direct Capture Source (FFI Edition) - Robust Version. +//! +//! Direct integration with USB cameras via V4L2 + V4L2 M2M hardware encoder. +//! Features auto-reconnect and device discovery logic. + +use super::h264_utils::{H264Packetizer, AnnexBParser, parse_profile_level_id}; +use super::stream_config_v2::parse_v4l2_url; +use super::{InternalSourceConfig, MediaPacket, StateChangeEvent, StreamSource, StreamSourceState}; +use anyhow::Result; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::{RwLock, broadcast, mpsc}; +use tracing::{info, warn, error}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::time::Instant; + +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::RTCPFeedback; +#[cfg(feature = "source")] +use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters}; + +const CHANNEL_VIDEO_RTP: u8 = 0; + +// --- FFI Bindings --- +#[repr(C)] +struct V4L2BridgeContext { _private: [u8; 0] } + +#[derive(Clone, Copy)] +pub struct V4L2BridgePtr(pub *mut V4L2BridgeContext); +unsafe impl Send for V4L2BridgePtr {} +unsafe impl Sync for V4L2BridgePtr {} + +#[derive(Clone, Copy)] +struct SendPtr(pub *mut c_void); +unsafe impl Send for SendPtr {} +unsafe impl Sync for SendPtr {} + +type V4L2NALCallbackFFI = unsafe extern "C" fn(data: *const u8, size: usize, is_keyframe: c_int, timestamp: u64, user_data: *mut c_void); + +unsafe extern "C" { + fn v4l2_bridge_init(device: *const c_char, width: c_int, height: c_int, fps: c_int, bitrate: c_int) -> *mut V4L2BridgeContext; + fn v4l2_bridge_set_callback(ctx: *mut V4L2BridgeContext, callback: V4L2NALCallbackFFI, user_data: *mut c_void); + fn v4l2_bridge_start(ctx: *mut V4L2BridgeContext) -> bool; + fn v4l2_bridge_stop(ctx: *mut V4L2BridgeContext); + fn v4l2_bridge_is_running(ctx: *mut V4L2BridgeContext) -> bool; + fn v4l2_bridge_request_keyframe(ctx: *mut V4L2BridgeContext); + fn v4l2_bridge_get_error(ctx: *mut V4L2BridgeContext) -> *const c_char; + fn v4l2_bridge_free(ctx: *mut V4L2BridgeContext); +} + +struct NALMessage { + data: Vec, + timestamp_us: u64, +} + +enum BridgeCommand { + RequestKeyframe, +} + +pub struct V4L2Source { + config: InternalSourceConfig, + device: String, + width: u32, + height: u32, + fps: u32, + bitrate: u32, + payload_type: u8, + clock_rate: u32, + profile: String, + state: Arc>, + rtp_tx: broadcast::Sender, + state_tx: broadcast::Sender, + task_handles: Vec>, + shutdown_tx: Option>, + cmd_tx: Option>, + #[cfg(feature = "source")] + dynamic_profile: Arc>>, +} + +unsafe impl Send for V4L2Source {} +unsafe impl Sync for V4L2Source {} + +impl V4L2Source { + pub fn from_url(url: &str, config: &crate::config::SourceConfig) -> Result { + let params = parse_v4l2_url(url)?; + let internal_config = InternalSourceConfig::from_config(config); + let (rtp_tx, _) = broadcast::channel(1024); + let (state_tx, _) = broadcast::channel(16); + + Ok(Self { + config: internal_config, + device: params.device, + width: params.width, + height: params.height, + fps: params.fps, + bitrate: params.bitrate, + payload_type: params.payload_type, + clock_rate: params.clock_rate, + profile: params.profile, + state: Arc::new(RwLock::new(StreamSourceState::Initializing)), + rtp_tx, + state_tx, + task_handles: Vec::new(), + shutdown_tx: None, + cmd_tx: None, + #[cfg(feature = "source")] + dynamic_profile: Arc::new(RwLock::new(None)), + }) + } + + async fn set_state(state_arc: &Arc>, state_tx: &broadcast::Sender, stream_id: &str, new_state: StreamSourceState, error: Option) { + let mut state = state_arc.write().await; + let old_state = *state; + if old_state != new_state { + *state = new_state; + let _ = state_tx.send(StateChangeEvent { old_state, new_state, error: error.clone() }); + info!("[{}] V4L2 state: {:?} -> {:?}{}", stream_id, old_state, new_state, error.map(|e| format!(" ({})", e)).unwrap_or_default()); + } + } + + unsafe extern "C" fn nal_callback(data: *const u8, size: usize, _is_kf: c_int, ts: u64, user_data: *mut c_void) { + if user_data.is_null() { return; } + let tx = &*(user_data as *const mpsc::UnboundedSender); + let buf = std::slice::from_raw_parts(data, size); + let _ = tx.send(NALMessage { data: buf.to_vec(), timestamp_us: ts }); + } + + #[cfg(feature = "source")] + async fn build_video_codec_params(&self) -> RTCRtpCodecParameters { + let profile = self.dynamic_profile.read().await.clone().unwrap_or_else(|| self.profile.clone()); + RTCRtpCodecParameters { + capability: RTCRtpCodecCapability { + mime_type: "video/H264".to_string(), + clock_rate: self.clock_rate, + channels: 0, + sdp_fmtp_line: format!("level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}", profile), + rtcp_feedback: vec![ + RTCPFeedback { typ: "goog-remb".into(), parameter: "".into() }, + RTCPFeedback { typ: "nack".into(), parameter: "".into() }, + RTCPFeedback { typ: "nack".into(), parameter: "pli".into() }, + ], + }, + payload_type: self.payload_type, + stats_id: String::new(), + } + } +} + +#[async_trait] +impl StreamSource for V4L2Source { + fn stream_id(&self) -> &str { &self.config.stream_id } + fn state(&self) -> StreamSourceState { *self.state.blocking_read() } + + async fn start(&mut self) -> Result<()> { + if self.shutdown_tx.is_some() { anyhow::bail!("Already started"); } + + let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1); + let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::(); + + self.shutdown_tx = Some(shutdown_tx); + self.cmd_tx = Some(cmd_tx); + + let stream_id = self.config.stream_id.clone(); + let device_path = self.device.clone(); + let width = self.width; + let height = self.height; + let fps = self.fps; + let bitrate = self.bitrate; + let payload_type = self.payload_type; + let clock_rate = self.clock_rate; + let rtp_tx = self.rtp_tx.clone(); + let state_tx = self.state_tx.clone(); + let state_arc = self.state.clone(); + #[cfg(feature = "source")] + let dynamic_profile = self.dynamic_profile.clone(); + + self.task_handles.push(tokio::spawn(async move { + let mut packetizer = H264Packetizer::new(1400, payload_type, clock_rate); + let mut parser = AnnexBParser::new(); + let start_inst = Instant::now(); + let mut last_rtp_ts: u32 = 0; + + loop { + info!("[{}] Attempting to open V4L2 device: {}", stream_id, device_path); + Self::set_state(&state_arc, &state_tx, &stream_id, StreamSourceState::Initializing, None).await; + + let device_cstr = match CString::new(device_path.clone()) { + Ok(s) => s, + Err(_) => { error!("[{}] Invalid device path", stream_id); break; } + }; + + let ctx = unsafe { + let ptr = v4l2_bridge_init(device_cstr.as_ptr(), width as c_int, height as c_int, fps as c_int, bitrate as c_int); + if ptr.is_null() { + V4L2BridgePtr(std::ptr::null_mut()) + } else { + V4L2BridgePtr(ptr) + } + }; + + if ctx.0.is_null() { + warn!("[{}] Failed to init bridge. Retrying in 5s...", stream_id); + tokio::select! { + _ = shutdown_rx.recv() => return, + _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => continue, + } + } + + let (nal_tx, mut nal_rx) = mpsc::unbounded_channel::(); + let wrapper = SendPtr(Box::into_raw(Box::new(nal_tx)) as *mut c_void); + + unsafe { + v4l2_bridge_set_callback(ctx.0, Self::nal_callback, wrapper.0); + if !v4l2_bridge_start(ctx.0) { + warn!("[{}] Bridge failed to start. Cleaning up...", stream_id); + v4l2_bridge_free(ctx.0); + let _ = Box::from_raw(wrapper.0 as *mut mpsc::UnboundedSender); + tokio::select! { + _ = shutdown_rx.recv() => return, + _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => continue, + } + } + } + + Self::set_state(&state_arc, &state_tx, &stream_id, StreamSourceState::Connected, None).await; + + let mut health_check = tokio::time::interval(std::time::Duration::from_secs(1)); + let mut session_active = true; + + while session_active { + tokio::select! { + _ = shutdown_rx.recv() => { + session_active = false; + } + _ = health_check.tick() => { + if unsafe { !v4l2_bridge_is_running(ctx.0) } { + warn!("[{}] Bridge health check failed. Restarting session...", stream_id); + session_active = false; + } + } + cmd = cmd_rx.recv() => { + if let Some(BridgeCommand::RequestKeyframe) = cmd { + unsafe { v4l2_bridge_request_keyframe(ctx.0); } + } + } + msg = nal_rx.recv() => { + if let Some(msg) = msg { + let rtp_ts = if msg.timestamp_us > 0 { + (msg.timestamp_us * 9 / 100) as u32 + } else { + (start_inst.elapsed().as_micros() * 9 / 100) as u32 + }; + let final_ts = if rtp_ts > last_rtp_ts { rtp_ts } else { last_rtp_ts.wrapping_add(3000) }; + last_rtp_ts = final_ts; + + parser.push(&msg.data); + let nals = parser.extract_nals(); + + for nal in nals { + #[cfg(feature = "source")] + if nal.nal_type == super::h264_utils::NalType::Sps { + if let Some(profile) = parse_profile_level_id(&nal.data) { + let mut dp = dynamic_profile.write().await; + if dp.as_ref() != Some(&profile) { + info!("[V4L2] SPS Profile refreshed: {}", profile); + *dp = Some(profile); + } + } + } + packetizer.advance_timestamp(final_ts.wrapping_sub(packetizer.get_current_timestamp())); + let rtp_packets = packetizer.packetize(&nal); + for packet in rtp_packets { + let _ = rtp_tx.send(MediaPacket::Rtp { + channel: CHANNEL_VIDEO_RTP, + data: packet.to_bytes(), + }); + } + } + } else { + session_active = false; + } + } + } + } + + unsafe { + v4l2_bridge_stop(ctx.0); + v4l2_bridge_free(ctx.0); + let _ = Box::from_raw(wrapper.0 as *mut mpsc::UnboundedSender); + } + + if shutdown_rx.try_recv().is_ok() { break; } + Self::set_state(&state_arc, &state_tx, &stream_id, StreamSourceState::Error, Some("Session lost, retrying...".into())).await; + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + } + })); + + Ok(()) + } + + async fn stop(&mut self) -> Result<()> { + if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } + for h in self.task_handles.drain(..) { let _ = h.await; } + self.cmd_tx = None; + let old_state = *self.state.read().await; + if old_state != StreamSourceState::Disconnected { + let mut state = self.state.write().await; + *state = StreamSourceState::Disconnected; + let _ = self.state_tx.send(StateChangeEvent { old_state, new_state: StreamSourceState::Disconnected, error: None }); + } + Ok(()) + } + + fn subscribe_rtp(&self) -> broadcast::Receiver { self.rtp_tx.subscribe() } + fn subscribe_state(&self) -> broadcast::Receiver { self.state_tx.subscribe() } + #[cfg(feature = "source")] + async fn get_video_codec(&self) -> Option { Some(self.build_video_codec_params().await) } + #[cfg(feature = "source")] + async fn get_audio_codec(&self) -> Option { None } + + #[cfg(feature = "source")] + async fn get_rtcp_sender(&self) -> Option>> { + let cmd_tx = self.cmd_tx.as_ref()?.clone(); + let (tx, mut rx) = mpsc::unbounded_channel::>(); + tokio::spawn(async move { + while let Some(data) = rx.recv().await { + if let Ok(packets) = webrtc::rtcp::packet::unmarshal(&mut &data[..]) { + for packet in packets { + if packet.as_any().downcast_ref::().is_some() { + let _ = cmd_tx.send(BridgeCommand::RequestKeyframe); + } + } + } + } + }); + Some(tx) + } +} diff --git a/livesrc/Cargo.toml b/livesrc/Cargo.toml new file mode 100644 index 00000000..06c03ed9 --- /dev/null +++ b/livesrc/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "livesrc" +description = "Lightweight camera source with WHEP streaming for edge devices" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +crate-type = ["lib"] + +[[bin]] +name = "livesrc" +path = "src/bin/livesrc.rs" + +[dependencies] +cli = { path = "../libs/cli" } + +anyhow = { workspace = true } +axum = { workspace = true } +clap = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["serde_derive"] } +tokio = { workspace = true, features = ["full"] } +tower-http = { workspace = true, features = ["trace", "cors"] } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +webrtc = { workspace = true } + +toml = "0.9" +reqwest = { workspace = true } +rand = "0.8" # For RTP SSRC/sequence generation + +# V4L2 support (optional) +v4l = { version = "0.14", optional = true } +bytes = "1" + +[features] +default = [] +v4l2 = ["dep:v4l"] diff --git a/livesrc/libcamera-bridge/CMakeLists.txt b/livesrc/libcamera-bridge/CMakeLists.txt new file mode 100644 index 00000000..2c2a4cb4 --- /dev/null +++ b/livesrc/libcamera-bridge/CMakeLists.txt @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.15) +project(libcamera-bridge VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(PLATFORM_RDK "Build for Horizon RDK X5" OFF) + +if(PLATFORM_RDK) + message(STATUS "Configuring for Horizon RDK X5 Platform") + set(SOURCES + encoder_rdk.cpp + v4l2_capture_rdk.cpp + bridge_v4l2_rdk_ffi.cpp + ) + add_definitions(-DPLATFORM_RDK) + + # RDK specific libraries (Sunrise 5) + link_directories(/usr/hobot/lib) + set(HB_LIBS multimedia hbmem vpf) +else() + message(STATUS "Configuring for Raspberry Pi Platform") + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBCAMERA REQUIRED libcamera) + + set(SOURCES + camera.cpp + encoder.cpp + bridge_ffi.cpp + v4l2_capture.cpp + bridge_v4l2_ffi.cpp + ) +endif() + +# Static Library for Rust FFI +add_library(cambridge STATIC ${SOURCES}) + +target_include_directories(cambridge PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${LIBCAMERA_INCLUDE_DIRS} + /usr/hobot/include + ) + +if(PLATFORM_RDK) + target_link_libraries(cambridge PRIVATE + ${HB_LIBS} + pthread + ) +else() + target_link_libraries(cambridge PRIVATE + ${LIBCAMERA_LIBRARIES} + pthread + ) +endif() + +target_compile_options(cambridge PRIVATE + -Wall + -Wextra + ${LIBCAMERA_CFLAGS_OTHER} +) + +# Install rules for the static library +install(TARGETS cambridge + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) diff --git a/livesrc/libcamera-bridge/README.md b/livesrc/libcamera-bridge/README.md new file mode 100644 index 00000000..97c8a87a --- /dev/null +++ b/livesrc/libcamera-bridge/README.md @@ -0,0 +1,63 @@ +# libcamera-bridge + +Direct libcamera API integration for LiveSrc - bypasses rpicam-vid and ffmpeg for lower latency. + +## Architecture + +``` +Raspberry Pi Camera (CSI) + ↓ +libcamera API + ↓ +Raw Frames (DMA Buffer) + ↓ +V4L2 M2M H.264 Encoder (GPU) + ↓ +H.264 NAL Units → stdout +``` + +## Build Requirements + +- libcamera-dev +- CMake >= 3.15 +- C++17 compiler + +## Build + +```bash +mkdir build +cd build +cmake .. +make +``` + +## Usage + +```bash +./libcamera-bridge \ + --width 640 \ + --height 480 \ + --fps 30 \ + --bitrate 2000000 \ + > output.h264 +``` + +## Integration with LiveSrc + +LibcameraSource will spawn this process and read H.264 from stdout. + +## Status + +🚧 **Work in Progress** + +- [x] Project structure +- [x] CMake configuration +- [x] CLI interface +- [ ] libcamera API integration (camera.cpp) +- [ ] V4L2 M2M encoder (encoder.cpp) +- [ ] DMA buffer management +- [ ] Testing on Raspberry Pi + +## Reference + +Based on [mediamtx-rpicamera](https://github.com/bluenviron/mediamtx-rpicamera) diff --git a/livesrc/libcamera-bridge/bridge_ffi.cpp b/livesrc/libcamera-bridge/bridge_ffi.cpp new file mode 100644 index 00000000..a931d883 --- /dev/null +++ b/livesrc/libcamera-bridge/bridge_ffi.cpp @@ -0,0 +1,95 @@ +#include "camera.h" +#include "encoder.h" +#include +#include +#include +#include + +extern "C" { + +struct BridgeContext { + uint32_t magic = 0xDEADBEEF; + CameraHandle camera; + Encoder encoder; + void (*on_frame)(const uint8_t* data, size_t size, int is_keyframe, uint64_t timestamp, void* user_data); + void* user_data; +}; + +// THE STABLE CHANNEL: Data from Camera -> Encoder +static void on_camera_frame_stable(const uint8_t* data, size_t size, uint64_t timestamp, void* user_data) { + if (!user_data) return; + auto ctx = static_cast(user_data); + if (ctx->magic != 0xDEADBEEF) return; + ctx->encoder.encode(data, size, timestamp); +} + +BridgeContext* bridge_init(int width, int height, int fps, int bitrate, int camera_id, int rotation, int hflip, int vflip) { + auto ctx = new BridgeContext(); + + CameraParams params; + params.width = width; + params.height = height; + params.fps = fps; + params.bitrate = bitrate; + params.camera_id = camera_id; + params.rotation = rotation; + params.hflip = hflip != 0; + params.vflip = vflip != 0; + + if (!ctx->encoder.init(params)) { + delete ctx; + return nullptr; + } + + ctx->camera = camera_create(); + if (!camera_init(ctx->camera, ¶ms)) { + camera_destroy(ctx->camera); + delete ctx; + return nullptr; + } + + camera_set_callback(ctx->camera, on_camera_frame_stable, ctx); + return ctx; +} + +void bridge_set_callback(BridgeContext* ctx, void (*callback)(const uint8_t*, size_t, int, uint64_t, void*), void* user_data) { + if (!ctx || ctx->magic != 0xDEADBEEF) return; + ctx->on_frame = callback; + ctx->user_data = user_data; + + ctx->encoder.setNALCallback([](const uint8_t* d, size_t s, int k, uint64_t ts, void* ud) { + auto c = static_cast(ud); + if (c->on_frame) { + c->on_frame(d, s, k, ts, c->user_data); + } + }, ctx); +} + +bool bridge_start(BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xDEADBEEF) return false; + return camera_start(ctx->camera); +} + +void bridge_stop(BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xDEADBEEF) return; + camera_stop(ctx->camera); + ctx->encoder.stop(); +} + +void bridge_request_keyframe(BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xDEADBEEF) return; + ctx->encoder.requestKeyframe(); +} + +const char* bridge_get_error(BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xDEADBEEF) return "Invalid Context"; + return camera_get_error(ctx->camera); +} + +void bridge_free(BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xDEADBEEF) return; + camera_destroy(ctx->camera); + delete ctx; +} + +} // extern "C" diff --git a/livesrc/libcamera-bridge/bridge_ffi.h b/livesrc/libcamera-bridge/bridge_ffi.h new file mode 100644 index 00000000..4696dcf6 --- /dev/null +++ b/livesrc/libcamera-bridge/bridge_ffi.h @@ -0,0 +1,53 @@ +#ifndef BRIDGE_FFI_H +#define BRIDGE_FFI_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Opaque handle for the bridge +typedef struct BridgeContext BridgeContext; + +// Callback type for H.264 NAL units +typedef void (*NALCallbackFFI)(const uint8_t* data, size_t size, int is_keyframe, uint64_t timestamp, void* user_data); + +// Initialize the camera bridge +// Returns a handle on success, NULL on failure +BridgeContext* bridge_init( + int width, + int height, + int fps, + int bitrate, + int camera_id, + int rotation, + int hflip, + int vflip +); + +// Set the NAL callback +void bridge_set_callback(BridgeContext* ctx, NALCallbackFFI callback, void* user_data); + +// Start capture and encoding +bool bridge_start(BridgeContext* ctx); + +// Stop capture +void bridge_stop(BridgeContext* ctx); + +// Force an IDR frame (Instant Keyframe Request) +void bridge_request_keyframe(BridgeContext* ctx); + +// Get last error message +const char* bridge_get_error(BridgeContext* ctx); + +// Cleanup and free the bridge +void bridge_free(BridgeContext* ctx); + +#ifdef __cplusplus +} +#endif + +#endif // BRIDGE_FFI_H diff --git a/livesrc/libcamera-bridge/bridge_v4l2_ffi.cpp b/livesrc/libcamera-bridge/bridge_v4l2_ffi.cpp new file mode 100644 index 00000000..c72f7c5a --- /dev/null +++ b/livesrc/libcamera-bridge/bridge_v4l2_ffi.cpp @@ -0,0 +1,112 @@ +#include "v4l2_capture.h" +#include "encoder.h" +#include +#include + +extern "C" { + +struct V4L2BridgeContext { + uint32_t magic = 0xBEEFCAFE; + V4L2CaptureHandle capture; + Encoder encoder; + void (*on_frame)(const uint8_t* data, size_t size, int is_keyframe, uint64_t timestamp, void* user_data); + void* user_data; +}; + +// Stable channel: V4L2 Capture → Encoder +static void on_v4l2_frame(const uint8_t* data, size_t size, uint64_t timestamp, void* user_data) { + if (!user_data) return; + auto* ctx = static_cast(user_data); + if (ctx->magic != 0xBEEFCAFE) return; + ctx->encoder.encode(data, size, timestamp); +} + +V4L2BridgeContext* v4l2_bridge_init(const char* device, int width, int height, int fps, int bitrate) { + auto* ctx = new V4L2BridgeContext(); + + // Initialize the encoder (reuses the existing V4L2 M2M encoder) + CameraParams enc_params; + enc_params.width = width; + enc_params.height = height; + enc_params.fps = fps; + enc_params.bitrate = bitrate; + enc_params.camera_id = 0; + enc_params.rotation = 0; + enc_params.hflip = false; + enc_params.vflip = false; + + if (!ctx->encoder.init(enc_params)) { + fprintf(stderr, "[V4L2Bridge] Encoder init failed\n"); + delete ctx; + return nullptr; + } + + // Initialize V4L2 capture + ctx->capture = v4l2cap_create(); + V4L2CaptureParams cap_params; + cap_params.device = device; + cap_params.width = width; + cap_params.height = height; + cap_params.fps = fps; + cap_params.input_format = 0; // YUYV + + if (!v4l2cap_init(ctx->capture, &cap_params)) { + fprintf(stderr, "[V4L2Bridge] Capture init failed: %s\n", v4l2cap_get_error(ctx->capture)); + v4l2cap_destroy(ctx->capture); + delete ctx; + return nullptr; + } + + // Wire: Capture → Encoder + v4l2cap_set_callback(ctx->capture, on_v4l2_frame, ctx); + + fprintf(stderr, "[V4L2Bridge] Init OK: %s %dx%d@%dfps\n", device, width, height, fps); + return ctx; +} + +void v4l2_bridge_set_callback(V4L2BridgeContext* ctx, + void (*callback)(const uint8_t*, size_t, int, uint64_t, void*), void* user_data) { + if (!ctx || ctx->magic != 0xBEEFCAFE) return; + ctx->on_frame = callback; + ctx->user_data = user_data; + + ctx->encoder.setNALCallback([](const uint8_t* d, size_t s, int k, uint64_t ts, void* ud) { + auto* c = static_cast(ud); + if (c->on_frame) { + c->on_frame(d, s, k, ts, c->user_data); + } + }, ctx); +} + +bool v4l2_bridge_start(V4L2BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xBEEFCAFE) return false; + return v4l2cap_start(ctx->capture); +} + +void v4l2_bridge_stop(V4L2BridgeContext* ctx) { + if (!ctx) return; + v4l2cap_stop(ctx->capture); +} + +bool v4l2_bridge_is_running(V4L2BridgeContext* ctx) { + if (!ctx) return false; + return v4l2cap_is_running(ctx->capture); +} + +void v4l2_bridge_request_keyframe(V4L2BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xBEEFCAFE) return; + ctx->encoder.requestKeyframe(); +} + +const char* v4l2_bridge_get_error(V4L2BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xBEEFCAFE) return "Invalid Context"; + return v4l2cap_get_error(ctx->capture); +} + +void v4l2_bridge_free(V4L2BridgeContext* ctx) { + if (!ctx || ctx->magic != 0xBEEFCAFE) return; + v4l2cap_destroy(ctx->capture); + delete ctx; +} + +} // extern "C" diff --git a/livesrc/libcamera-bridge/bridge_v4l2_ffi.h b/livesrc/libcamera-bridge/bridge_v4l2_ffi.h new file mode 100644 index 00000000..f054acd9 --- /dev/null +++ b/livesrc/libcamera-bridge/bridge_v4l2_ffi.h @@ -0,0 +1,40 @@ +#ifndef BRIDGE_V4L2_FFI_H +#define BRIDGE_V4L2_FFI_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Opaque handle for the V4L2 bridge +typedef struct V4L2BridgeContext V4L2BridgeContext; + +// Callback type: same signature as the libcamera bridge for Rust compatibility +typedef void (*V4L2NALCallbackFFI)(const uint8_t* data, size_t size, int is_keyframe, uint64_t timestamp, void* user_data); + +// Initialize the V4L2 bridge (USB camera + hardware encoder) +// device: V4L2 device path, e.g. "/dev/video2" +V4L2BridgeContext* v4l2_bridge_init( + const char* device, + int width, + int height, + int fps, + int bitrate +); + +void v4l2_bridge_set_callback(V4L2BridgeContext* ctx, V4L2NALCallbackFFI callback, void* user_data); +bool v4l2_bridge_start(V4L2BridgeContext* ctx); +void v4l2_bridge_stop(V4L2BridgeContext* ctx); +bool v4l2_bridge_is_running(V4L2BridgeContext* ctx); +void v4l2_bridge_request_keyframe(V4L2BridgeContext* ctx); +const char* v4l2_bridge_get_error(V4L2BridgeContext* ctx); +void v4l2_bridge_free(V4L2BridgeContext* ctx); + +#ifdef __cplusplus +} +#endif + +#endif // BRIDGE_V4L2_FFI_H diff --git a/livesrc/libcamera-bridge/bridge_v4l2_rdk_ffi.cpp b/livesrc/libcamera-bridge/bridge_v4l2_rdk_ffi.cpp new file mode 100644 index 00000000..440ad08c --- /dev/null +++ b/livesrc/libcamera-bridge/bridge_v4l2_rdk_ffi.cpp @@ -0,0 +1,122 @@ +#include "bridge_ffi.h" +#include "encoder.h" +#include +#include +#include + +extern "C" { + #include "v4l2_capture.h" +} + +// 内部结构体定义 +struct V4L2BridgeContext { + V4L2CaptureHandle capture = nullptr; + Encoder* encoder = nullptr; + NALCallbackFFI callback = nullptr; + void* user_data = nullptr; +}; + +extern "C" { + +// 1. 实现 Rust 侧调用的 V4L2 接口 +V4L2BridgeContext* v4l2_bridge_init(const char* device, int width, int height, int fps, int bitrate) { + auto* ctx = new V4L2BridgeContext(); + ctx->encoder = new Encoder(); + + V4L2CaptureParams cap_params; + cap_params.device = device; + cap_params.width = width; + cap_params.height = height; + cap_params.fps = fps; + cap_params.input_format = 0; // YUYV + + ctx->capture = v4l2cap_create(); + if (!v4l2cap_init(ctx->capture, &cap_params)) { + fprintf(stderr, "[V4L2Bridge-RDK] Capture init failed for %s: %s\n", device, v4l2cap_get_error(ctx->capture)); + delete ctx->encoder; + v4l2cap_destroy(ctx->capture); + delete ctx; + return nullptr; + } + + CameraParams enc_params; + enc_params.width = width; + enc_params.height = height; + enc_params.fps = fps; + enc_params.bitrate = bitrate; + + if (!ctx->encoder->init(enc_params)) { + fprintf(stderr, "[V4L2Bridge-RDK] Encoder init failed\n"); + v4l2cap_destroy(ctx->capture); + delete ctx->encoder; + delete ctx; + return nullptr; + } + + // 设置回调链 + v4l2cap_set_callback(ctx->capture, [](const uint8_t* data, size_t size, uint64_t ts_us, void* user) { + auto* c = static_cast(user); + if (data && c->encoder) { + c->encoder->encode(data, size, ts_us); + } + }, ctx); + + ctx->encoder->setNALCallback([](const uint8_t* data, size_t size, int is_kf, uint64_t ts, void* user) { + auto* c = static_cast(user); + if (c->callback) { + c->callback(data, size, is_kf, ts, c->user_data); + } + }, ctx); + + return ctx; +} + +void v4l2_bridge_set_callback(V4L2BridgeContext* ctx, NALCallbackFFI callback, void* user_data) { + if (ctx) { + ctx->callback = callback; + ctx->user_data = user_data; + } +} + +bool v4l2_bridge_start(V4L2BridgeContext* ctx) { + if (!ctx) return false; + return v4l2cap_start(ctx->capture); +} + +void v4l2_bridge_stop(V4L2BridgeContext* ctx) { + if (ctx) v4l2cap_stop(ctx->capture); +} + +bool v4l2_bridge_is_running(V4L2BridgeContext* ctx) { + return ctx && v4l2cap_is_running(ctx->capture); +} + +void v4l2_bridge_request_keyframe(V4L2BridgeContext* ctx) { + if (ctx && ctx->encoder) ctx->encoder->requestKeyframe(); +} + +const char* v4l2_bridge_get_error(V4L2BridgeContext* ctx) { + return ctx ? v4l2cap_get_error(ctx->capture) : "Invalid context"; +} + +void v4l2_bridge_free(V4L2BridgeContext* ctx) { + if (ctx) { + v4l2cap_destroy(ctx->capture); + if (ctx->encoder) delete ctx->encoder; + delete ctx; + } +} + +// 2. 修复 bridge_ffi.h 中定义的 stubs (必须严格按照函数签名) +BridgeContext* bridge_init(int w, int h, int f, int b, int ci, int rot, int hf, int vf) { return nullptr; } +void bridge_cleanup() {} +void bridge_start_libcamera() {} +void bridge_stop_libcamera() {} +const char* bridge_get_error(BridgeContext* ctx) { return ""; } +void bridge_request_keyframe(BridgeContext* ctx) {} +void bridge_set_callback(BridgeContext* ctx, NALCallbackFFI cb, void* ud) {} +bool bridge_start(BridgeContext* ctx) { return false; } +void bridge_stop(BridgeContext* ctx) {} +void bridge_free(BridgeContext* ctx) {} + +} // extern "C" diff --git a/livesrc/libcamera-bridge/build.sh b/livesrc/libcamera-bridge/build.sh new file mode 100644 index 00000000..3847078e --- /dev/null +++ b/livesrc/libcamera-bridge/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Build script for libcamera-bridge + +set -e + +echo "=== Building libcamera-bridge ===" + +# Create build directory +mkdir -p build +cd build + +# Configure +echo "Configuring..." +cmake .. + +# Build +echo "Building..." +make -j$(nproc) + +echo "=== Build complete ===" +echo "Binary: build/libcamera-bridge" +echo "" +echo "Test with:" +echo " ./build/libcamera-bridge --width 640 --height 480 --fps 30 --bitrate 2000000 > test.h264" diff --git a/livesrc/libcamera-bridge/camera.cpp b/livesrc/libcamera-bridge/camera.cpp new file mode 100644 index 00000000..c0d73bc5 --- /dev/null +++ b/livesrc/libcamera-bridge/camera.cpp @@ -0,0 +1,187 @@ +#include "camera.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace libcamera; + +// Private implementation class +struct FastMapping { + Request* req; + uint8_t* data; + size_t size; + void* impl; +}; + +#define MAX_REGISTRY 16 +static FastMapping g_registry[MAX_REGISTRY]; +static volatile int g_registry_count = 0; + +class PiCameraImpl { +public: + uint32_t magic = 0xBCBCBCBC; + std::unique_ptr cameraManager; + std::shared_ptr camera; + Stream* videoStream = nullptr; + std::vector> requests; + GlobalFrameCallback callback = nullptr; + void* userData = nullptr; + bool running = false; + int target_fps = 30; // V14.9-TURBO: Store FPS +}; + +// THE V14.6 "SCREAMING" SLOT +static void stable_slot_v14_6(Request* request) { + if (!request) return; + + // SCERAM: I AM ALIVE! + static int global_counter = 0; + if (global_counter % 10 == 0) { + fprintf(stderr, "[CameraInternal] SLOT TRIGGERED! req=%p, status=%d\n", request, request->status()); + } + global_counter++; + + FastMapping* entry = nullptr; + for (int i = 0; i < g_registry_count; ++i) { + if (g_registry[i].req == request) { + entry = &g_registry[i]; + break; + } + } + + if (entry && entry->impl) { + PiCameraImpl* impl = static_cast(entry->impl); + if (impl->magic == 0xBCBCBCBC) { + static auto start_time = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + uint64_t timestamp = std::chrono::duration_cast(now - start_time).count(); + + if (impl->callback) { + impl->callback(entry->data, entry->size, timestamp, impl->userData); + } + } + + // Reuse and RE-QUEUE + request->reuse(Request::ReuseFlag::ReuseBuffers); + if (impl->running) { + impl->camera->queueRequest(request); + } + } else { + fprintf(stderr, "[CameraInternal] Registry MISS for req=%p! (RegistryCount=%d)\n", request, g_registry_count); + } +} + +extern "C" { + +CameraHandle camera_create() { + return new PiCameraImpl(); +} + +void camera_destroy(CameraHandle handle) { + if (!handle) return; + delete static_cast(handle); +} + +bool camera_init(CameraHandle handle, const CameraParams* params) { + if (!handle || !params) return false; + PiCameraImpl* impl = static_cast(handle); + impl->target_fps = params->fps; // V14.9-TURBO: Save the target FPS + + impl->cameraManager = std::make_unique(); + if (impl->cameraManager->start() != 0) return false; + if (impl->cameraManager->cameras().empty()) return false; + impl->camera = impl->cameraManager->get(impl->cameraManager->cameras()[0]->id()); + if (!impl->camera || impl->camera->acquire() != 0) return false; + + std::unique_ptr config = impl->camera->generateConfiguration({StreamRole::VideoRecording}); + StreamConfiguration& sc = config->at(0); + sc.size.width = params->width; + sc.size.height = params->height; + sc.pixelFormat = formats::YUV420; + sc.bufferCount = 8; + + if (config->validate() == CameraConfiguration::Invalid) { + fprintf(stderr, "[CameraInternal] Config was invalid, adjusted.\n"); + } + fprintf(stderr, "[CameraInternal] Validated Format: %s, %dx%d\n", sc.pixelFormat.toString().c_str(), sc.size.width, sc.size.height); + + if (impl->camera->configure(config.get()) < 0) return false; + + impl->videoStream = sc.stream(); + FrameBufferAllocator* allocator = new FrameBufferAllocator(impl->camera); + if (allocator->allocate(impl->videoStream) < 0) return false; + + g_registry_count = 0; + for (const auto& buffer : allocator->buffers(impl->videoStream)) { + FrameBuffer* ptr = buffer.get(); + size_t s = 0; + for (const auto& p : ptr->planes()) s += p.length; + + // Map as single block - standard for Pi YUV + void* d = mmap(NULL, s, PROT_READ, MAP_SHARED, ptr->planes()[0].fd.get(), 0); + + std::unique_ptr r = impl->camera->createRequest(); + r->addBuffer(impl->videoStream, ptr); + + if (g_registry_count < MAX_REGISTRY) { + g_registry[g_registry_count].req = r.get(); + g_registry[g_registry_count].data = static_cast(d); + g_registry[g_registry_count].size = s; + g_registry[g_registry_count].impl = impl; + fprintf(stderr, "[Registry] [%d] Request=%p, Map=%p\n", g_registry_count, r.get(), d); + g_registry_count++; + } + impl->requests.push_back(std::move(r)); + } + return true; +} + +bool camera_start(CameraHandle handle) { + if (!handle) return false; + PiCameraImpl* impl = static_cast(handle); + impl->camera->requestCompleted.connect(stable_slot_v14_6); + + // V14.9-TURBO: Force 30 FPS by locking FrameDurationLimits + ControlList controls; + int64_t frame_duration = 1000000 / impl->target_fps; + controls.set(controls::FrameDurationLimits, { frame_duration, frame_duration }); + + int ret = impl->camera->start(&controls); + fprintf(stderr, "[CameraInternal] Camera Start Return: %d\n", ret); + if (ret < 0) return false; + + impl->running = true; + for (auto& r : impl->requests) { + int qret = impl->camera->queueRequest(r.get()); + if (qret < 0) fprintf(stderr, "[CameraInternal] Queue FAILED: %d\n", qret); + } + return true; +} + +void camera_stop(CameraHandle handle) { + if (!handle) return; + PiCameraImpl* impl = static_cast(handle); + impl->running = false; + impl->camera->stop(); + impl->camera->requestCompleted.disconnect(stable_slot_v14_6); +} + +void camera_set_callback(CameraHandle handle, GlobalFrameCallback callback, void* user_data) { + if (!handle) return; + PiCameraImpl* impl = static_cast(handle); + impl->callback = callback; + impl->userData = user_data; +} + +const char* camera_get_error(CameraHandle handle) { + (void)handle; return ""; +} + +} // extern "C" diff --git a/livesrc/libcamera-bridge/camera.h b/livesrc/libcamera-bridge/camera.h new file mode 100644 index 00000000..29eb7e94 --- /dev/null +++ b/livesrc/libcamera-bridge/camera.h @@ -0,0 +1,34 @@ +#ifndef CAMERA_H +#define CAMERA_H + +#include +#include + +struct CameraParams { + int width; + int height; + int fps; + int bitrate; + int camera_id; + int rotation; + bool hflip; + bool vflip; +}; + +// Raw Global Callback Type +typedef void (*GlobalFrameCallback)(const uint8_t* data, size_t size, uint64_t timestamp, void* user_data); + +// Opaque handle for C++ implementation +typedef void* CameraHandle; + +extern "C" { + CameraHandle camera_create(); + void camera_destroy(CameraHandle handle); + bool camera_init(CameraHandle handle, const CameraParams* params); + bool camera_start(CameraHandle handle); + void camera_stop(CameraHandle handle); + void camera_set_callback(CameraHandle handle, GlobalFrameCallback callback, void* user_data); + const char* camera_get_error(CameraHandle handle); +} + +#endif // CAMERA_H diff --git a/livesrc/libcamera-bridge/deploy.sh b/livesrc/libcamera-bridge/deploy.sh new file mode 100644 index 00000000..b7f246fc --- /dev/null +++ b/livesrc/libcamera-bridge/deploy.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Deploy script - transfer to Raspberry Pi and build + +PI_USER="hao" +PI_HOST="192.168.132.253" +PI_PATH="~/livesrc/libcamera-bridge" + +echo "=== Deploying libcamera-bridge to Raspberry Pi ===" + +# Create remote directory +echo "Creating remote directory..." +ssh ${PI_USER}@${PI_HOST} "mkdir -p ${PI_PATH}" + +# Transfer source files +echo "Transferring files..." +scp -r \ + CMakeLists.txt \ + *.h \ + *.cpp \ + build.sh \ + README.md \ + ${PI_USER}@${PI_HOST}:${PI_PATH}/ + +echo "=== Transfer complete ===" +echo "" +echo "Run on Raspberry Pi:" +echo " ssh ${PI_USER}@${PI_HOST}" +echo " cd ${PI_PATH}" +echo " ./build.sh" +echo " ./build/libcamera-bridge --width 640 --height 480 --fps 30 > test.h264" diff --git a/livesrc/libcamera-bridge/encoder.cpp b/livesrc/libcamera-bridge/encoder.cpp new file mode 100644 index 00000000..13cfb47a --- /dev/null +++ b/livesrc/libcamera-bridge/encoder.cpp @@ -0,0 +1,239 @@ +#include "encoder.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Encoder::Impl { +public: + int fd = -1; + uint32_t width = 0; + uint32_t height = 0; + uint32_t fps = 0; + uint32_t bitrate = 0; + + NALCallbackFFI nalCallback = nullptr; + void* userData = nullptr; + std::string errorMsg; + + struct Buffer { + void* start; + size_t length; + }; + + std::vector inputBuffers; + std::vector outputBuffers; + std::queue freeInputIndices; + bool force_idr = false; + int frames_injected = 0; + int frames_dropped = 0; + + bool setupV4L2(const CameraParams& params); + void cleanup(); +}; + +Encoder::Encoder() : pImpl(std::make_unique()) {} + +Encoder::~Encoder() { + pImpl->cleanup(); +} + +void Encoder::Impl::cleanup() { + if (fd >= 0) { + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + ioctl(fd, VIDIOC_STREAMOFF, &type); + type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + ioctl(fd, VIDIOC_STREAMOFF, &type); + + for (auto& buf : inputBuffers) { + if (buf.start && buf.start != MAP_FAILED) munmap(buf.start, buf.length); + } + for (auto& buf : outputBuffers) { + if (buf.start && buf.start != MAP_FAILED) munmap(buf.start, buf.length); + } + + close(fd); + fd = -1; + } +} + +bool Encoder::init(const CameraParams& params) { + return pImpl->setupV4L2(params); +} + +bool Encoder::Impl::setupV4L2(const CameraParams& params) { + fprintf(stderr, "[Encoder] Initializing V4L2 M2M (V14.9-TURBO)...\n"); + fd = open("/dev/video11", O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) return false; + + width = params.width; + height = params.height; + fps = params.fps; + bitrate = params.bitrate; + + struct v4l2_format fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + fmt.fmt.pix_mp.width = width; + fmt.fmt.pix_mp.height = height; + fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420; + fmt.fmt.pix_mp.field = V4L2_FIELD_NONE; + fmt.fmt.pix_mp.num_planes = 1; + + if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) return false; + + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264; + if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) return false; + + struct v4l2_control ctrl = {}; + ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE; + ctrl.value = bitrate; + ioctl(fd, VIDIOC_S_CTRL, &ctrl); + + ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD; + ctrl.value = fps * 2; + ioctl(fd, VIDIOC_S_CTRL, &ctrl); + + ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER; + ctrl.value = 1; + ioctl(fd, VIDIOC_S_CTRL, &ctrl); + + // V14.9 TURBO Depth: 8 (Essential for 30FPS H264 on Pi 5) + struct v4l2_requestbuffers req = {}; + req.count = 8; + req.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + req.memory = V4L2_MEMORY_MMAP; + if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) return false; + + for (unsigned int i = 0; i < req.count; i++) { + struct v4l2_buffer buf = {}; + struct v4l2_plane planes[1] = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + buf.length = 1; + buf.m.planes = planes; + if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) return false; + void* start = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, planes[0].m.mem_offset); + inputBuffers.push_back({start, planes[0].length}); + freeInputIndices.push(i); + } + + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) return false; + for (unsigned int i = 0; i < req.count; i++) { + struct v4l2_buffer buf = {}; + struct v4l2_plane planes[1] = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + buf.length = 1; + buf.m.planes = planes; + if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) return false; + void* start = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, planes[0].m.mem_offset); + outputBuffers.push_back({start, planes[0].length}); + if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) return false; + } + + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + ioctl(fd, VIDIOC_STREAMON, &type); + type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + ioctl(fd, VIDIOC_STREAMON, &type); + + fprintf(stderr, "[Encoder] Turbo Pipeline Primed (Queue: 8)\n"); + return true; +} + +void Encoder::encode(const uint8_t* data, size_t size, uint64_t timestamp) { + if (pImpl->fd < 0) return; + + // 1. DRAIN OUTPUT POOL + struct v4l2_buffer buf_out = {}; + struct v4l2_plane planes_out[1] = {}; + buf_out.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf_out.memory = V4L2_MEMORY_MMAP; + buf_out.length = 1; + buf_out.m.planes = planes_out; + + while (ioctl(pImpl->fd, VIDIOC_DQBUF, &buf_out) == 0) { + if (pImpl->nalCallback) { + uint8_t* encoded_data = static_cast(pImpl->outputBuffers[buf_out.index].start); + size_t encoded_size = planes_out[0].bytesused; + int is_keyframe = (buf_out.flags & V4L2_BUF_FLAG_KEYFRAME) ? 1 : 0; + + uint64_t ts_us = (uint64_t)buf_out.timestamp.tv_sec * 1000000 + buf_out.timestamp.tv_usec; + pImpl->nalCallback(encoded_data, encoded_size, is_keyframe, ts_us, pImpl->userData); + } + ioctl(pImpl->fd, VIDIOC_QBUF, &buf_out); + } + + // 2. RECLAIM INPUT POOL + struct v4l2_buffer buf_in = {}; + struct v4l2_plane planes_in[1] = {}; + buf_in.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf_in.memory = V4L2_MEMORY_MMAP; + buf_in.length = 1; + buf_in.m.planes = planes_in; + + while (ioctl(pImpl->fd, VIDIOC_DQBUF, &buf_in) == 0) { + pImpl->freeInputIndices.push(buf_in.index); + } + + // 3. FEED INPUT (Prioritize Queue depth) + if (!pImpl->freeInputIndices.empty()) { + int target_index = pImpl->freeInputIndices.front(); + pImpl->freeInputIndices.pop(); + + memcpy(pImpl->inputBuffers[target_index].start, data, size); + + buf_in.index = target_index; + planes_in[0].bytesused = size; + buf_in.timestamp.tv_sec = timestamp / 1000000; + buf_in.timestamp.tv_usec = timestamp % 1000000; + + if (pImpl->force_idr) { + struct v4l2_control ctrl = {}; + ctrl.id = V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME; + ctrl.value = 1; + ioctl(pImpl->fd, VIDIOC_S_CTRL, &ctrl); + pImpl->force_idr = false; + } + + if (ioctl(pImpl->fd, VIDIOC_QBUF, &buf_in) == 0) { + pImpl->frames_injected++; + if (pImpl->frames_injected % 30 == 0) { + fprintf(stderr, "[Encoder] IN: Frame %d (Turbo mode)\n", pImpl->frames_injected); + } + } + } else { + pImpl->frames_dropped++; + if (pImpl->frames_dropped % 30 == 0) { + fprintf(stderr, "[Encoder] WARNING: Buffer exhaustion, dropping frame %d!\n", pImpl->frames_dropped); + } + } +} + + +void Encoder::setNALCallback(NALCallbackFFI callback, void* user_data) { + pImpl->nalCallback = callback; + pImpl->userData = user_data; +} + +void Encoder::requestKeyframe() { + pImpl->force_idr = true; +} + +const char* Encoder::getError() const { + return pImpl->errorMsg.c_str(); +} + +void Encoder::stop() { + pImpl->cleanup(); +} diff --git a/livesrc/libcamera-bridge/encoder.h b/livesrc/libcamera-bridge/encoder.h new file mode 100644 index 00000000..6ebad06c --- /dev/null +++ b/livesrc/libcamera-bridge/encoder.h @@ -0,0 +1,28 @@ +#include "bridge_ffi.h" +#include "camera.h" +#include +#include +#include +#include + +class Encoder { +public: + Encoder(); + ~Encoder(); + + bool init(const CameraParams& params); + void encode(const uint8_t* data, size_t size, uint64_t timestamp); + void encodeShared(int dma_fd, size_t size, uint64_t timestamp); + void stop(); + void requestKeyframe(); + + // Direct FFI callback pointer from Rust + void setNALCallback(NALCallbackFFI callback, void* user_data); + + // Get last error message + const char* getError() const; + +private: + class Impl; + std::unique_ptr pImpl; +}; diff --git a/livesrc/libcamera-bridge/encoder_rdk.cpp b/livesrc/libcamera-bridge/encoder_rdk.cpp new file mode 100644 index 00000000..8bb7e18e --- /dev/null +++ b/livesrc/libcamera-bridge/encoder_rdk.cpp @@ -0,0 +1,148 @@ +#include "encoder.h" +#include +#include +#include +#include + +extern "C" { + #include "hb_media_codec.h" + #include "hb_media_error.h" +} + +// 优化的 NEON YUYV -> NV12 转换函数 +static void yuyv_to_nv12_neon(const uint8_t* yuyv, uint8_t* nv12_y, uint8_t* nv12_uv, int width, int height) { + int num_pixels = width * height; + for (int i = 0; i < num_pixels; i += 16) { + uint8x16x2_t raw = vld2q_u8(yuyv + i * 2); + vst1q_u8(nv12_y + i, raw.val[0]); + } + for (int i = 0; i < height; i += 2) { + const uint8_t* line = yuyv + i * width * 2; + uint8_t* dest_uv = nv12_uv + (i / 2) * width; + for (int j = 0; j < width; j += 16) { + uint8x16x4_t raw = vld4q_u8(line + j * 2); + uint8x16x2_t uv; + uv.val[0] = raw.val[1]; + uv.val[1] = raw.val[3]; + vst2q_u8(dest_uv + j, uv); + } + } +} + +struct Encoder::Impl { + media_codec_context_t* context = nullptr; + CameraParams params; + NALCallbackFFI p_callback = nullptr; + void* user_data = nullptr; + long frame_count = 0; + + ~Impl() { + if (context) { + hb_mm_mc_stop(context); + hb_mm_mc_release(context); + free(context); + } + } +}; + +Encoder::Encoder() : pImpl(std::make_unique()) {} +Encoder::~Encoder() = default; + +bool Encoder::init(const CameraParams& params) { + pImpl->params = params; + pImpl->context = (media_codec_context_t*)malloc(sizeof(media_codec_context_t)); + memset(pImpl->context, 0, sizeof(media_codec_context_t)); + + auto* ctx = pImpl->context; + ctx->encoder = true; + ctx->codec_id = MEDIA_CODEC_ID_H264; + + auto* v_params = &ctx->video_enc_params; + v_params->width = params.width; + v_params->height = params.height; + v_params->pix_fmt = MC_PIXEL_FORMAT_NV12; + v_params->bitstream_buf_size = (params.width * params.height * 3 / 2 + 4095) & ~4095; + v_params->frame_buf_count = 5; + v_params->bitstream_buf_count = 5; + v_params->gop_params.gop_preset_idx = 1; + v_params->enable_user_pts = 1; + + v_params->rc_params.mode = MC_AV_RC_MODE_H264CBR; + hb_mm_mc_get_rate_control_config(ctx, &v_params->rc_params); + v_params->rc_params.h264_cbr_params.intra_period = 30; + v_params->rc_params.h264_cbr_params.frame_rate = params.fps; + v_params->rc_params.h264_cbr_params.bit_rate = params.bitrate / 1000; + + if (hb_mm_mc_initialize(ctx) != 0) return false; + if (hb_mm_mc_configure(ctx) != 0) return false; + + mc_av_codec_startup_params_t startup_params; + memset(&startup_params, 0, sizeof(startup_params)); + return hb_mm_mc_start(ctx, &startup_params) == 0; +} + +void Encoder::encode(const uint8_t* data, size_t size, uint64_t timestamp) { + if (!pImpl->context) return; + + media_codec_buffer_t input_buf; + memset(&input_buf, 0, sizeof(media_codec_buffer_t)); + if (hb_mm_mc_dequeue_input_buffer(pImpl->context, &input_buf, 100) == 0) { + yuyv_to_nv12_neon(data, input_buf.vframe_buf.vir_ptr[0], input_buf.vframe_buf.vir_ptr[1], pImpl->params.width, pImpl->params.height); + input_buf.vframe_buf.pts = timestamp / 1000; + hb_mm_mc_queue_input_buffer(pImpl->context, &input_buf, 100); + } + + media_codec_buffer_t output_buf; + memset(&output_buf, 0, sizeof(media_codec_buffer_t)); + if (hb_mm_mc_dequeue_output_buffer(pImpl->context, &output_buf, NULL, 0) == 0) { + if (pImpl->p_callback) { + uint8_t* out_data = (uint8_t*)output_buf.vstream_buf.vir_ptr; + uint32_t out_len = output_buf.vstream_buf.size; + + // 关键帧检测逻辑:解析 H.264 NAL Unit Type + // 查找起始码 00 00 00 01 (Annex-B) + int is_kf = 0; + if (out_len > 5) { + for (uint32_t i = 0; i < out_len - 4; ++i) { + if (out_data[i] == 0 && out_data[i+1] == 0 && out_data[i+2] == 0 && out_data[i+3] == 1) { + int nal_type = out_data[i+4] & 0x1F; + if (nal_type == 5 || nal_type == 7 || nal_type == 8) { // IDR, SPS, or PPS + is_kf = 1; + break; + } + } + } + } + + pImpl->p_callback(out_data, out_len, is_kf, timestamp, pImpl->user_data); + } + hb_mm_mc_queue_output_buffer(pImpl->context, &output_buf, 100); + + if (++pImpl->frame_count % 150 == 0) { + printf("[V4L2-RDK] Encoding flow stable. Frames: %ld\n", pImpl->frame_count); + } + } +} + +void Encoder::encodeShared(int dma_fd, size_t size, uint64_t timestamp) { + // 暂未实现 +} + +void Encoder::stop() { + if (pImpl->context) { + hb_mm_mc_stop(pImpl->context); + } +} + +void Encoder::setNALCallback(NALCallbackFFI cb, void* user_data) { + pImpl->p_callback = cb; + pImpl->user_data = user_data; +} + +void Encoder::requestKeyframe() { + // RDK 会根据 intra_period 自动下发 +} + +const char* Encoder::getError() const { + return "Encoder error"; +} diff --git a/livesrc/libcamera-bridge/main.cpp b/livesrc/libcamera-bridge/main.cpp new file mode 100644 index 00000000..078391aa --- /dev/null +++ b/livesrc/libcamera-bridge/main.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#include +#include +#include "camera.h" +#include "encoder.h" + +std::atomic running(true); + +void signalHandler(int signal) { + if (signal == SIGINT || signal == SIGTERM) { + running = false; + } +} + +void printUsage(const char* prog) { + std::cerr << "Usage: " << prog << " [options]\n" + << "Options:\n" + << " --width WIDTH Video width (default: 640)\n" + << " --height HEIGHT Video height (default: 480)\n" + << " --fps FPS Frame rate (default: 30)\n" + << " --bitrate BITRATE Bitrate in bps (default: 2000000)\n" + << " --camera-id ID PiCamera ID (default: 0)\n" + << " --rotation DEGREES Rotation degrees: 0, 90, 180, 270 (default: 0)\n" + << " --hflip Horizontal flip\n" + << " --vflip Vertical flip\n" + << " --help Show this help\n" + << "\n" + << "Control commands (stdin):\n" + << " k - Request keyframe (IDR)\n"; +} + +int main(int argc, char* argv[]) { + // Default parameters + CameraParams camParams = { + .width = 640, + .height = 480, + .fps = 30, + .bitrate = 2000000, + .camera_id = 0, + .rotation = 0, + .hflip = false, + .vflip = false + }; + + // Parse command line arguments + static struct option long_options[] = { + {"width", required_argument, 0, 'w'}, + {"height", required_argument, 0, 'h'}, + {"fps", required_argument, 0, 'f'}, + {"bitrate", required_argument, 0, 'b'}, + {"camera-id", required_argument, 0, 'c'}, + {"rotation", required_argument, 0, 'r'}, + {"hflip", no_argument, 0, 'H'}, + {"vflip", no_argument, 0, 'V'}, + {"help", no_argument, 0, '?'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "w:h:f:b:c:r:HV?", long_options, nullptr)) != -1) { + switch (opt) { + case 'w': camParams.width = atoi(optarg); break; + case 'h': camParams.height = atoi(optarg); break; + case 'f': camParams.fps = atoi(optarg); break; + case 'b': camParams.bitrate = atoi(optarg); break; + case 'c': camParams.camera_id = atoi(optarg); break; + case 'r': camParams.rotation = atoi(optarg); break; + case 'H': camParams.hflip = true; break; + case 'V': camParams.vflip = true; break; + case '?': + default: + printUsage(argv[0]); + return 1; + } + } + + // Setup signal handlers + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + // CRITICAL: Set stdout to unbuffered mode for pipe communication + setbuf(stdout, NULL); + + // Set stdin to non-blocking mode for control commands + int flags = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); + + std::cerr << "libcamera-bridge starting...\n" + << " Resolution: " << camParams.width << "x" << camParams.height << "\n" + << " FPS: " << camParams.fps << "\n" + << " Bitrate: " << camParams.bitrate << " bps\n" + << " Control: stdin (send 'k' for keyframe)\n"; + + // Create camera + PiCamera camera; + if (!camera.init(camParams)) { + std::cerr << "Failed to initialize camera: " << camera.getError() << "\n"; + return 1; + } + + // Create encoder + EncoderParams encParams = { + .width = camParams.width, + .height = camParams.height, + .fps = camParams.fps, + .bitrate = camParams.bitrate + }; + + Encoder encoder; + if (!encoder.init(encParams)) { + std::cerr << "Failed to initialize encoder: " << encoder.getError() << "\n"; + return 1; + } + + // Set callbacks + camera.setFrameCallback([&encoder](const uint8_t* data, size_t size, uint64_t timestamp) { + // Forward raw frame to encoder + encoder.encode(data, size, timestamp); + }); + + encoder.setNALCallback([](const uint8_t* data, size_t size, bool is_keyframe) { + // Write H.264 NAL unit to stdout (Annex B format with start code) + (void)is_keyframe; // Unused for now - could be used for logging keyframes + static const uint8_t start_code[] = {0, 0, 0, 1}; + fwrite(start_code, 1, sizeof(start_code), stdout); + fwrite(data, 1, size, stdout); + fflush(stdout); + }); + + // Start camera + if (!camera.start()) { + std::cerr << "Failed to start camera: " << camera.getError() << "\n"; + return 1; + } + + std::cerr << "Streaming started. Press Ctrl+C to stop.\n"; + + // Main loop with stdin monitoring + while (running) { + // Check for stdin commands (non-blocking) + char cmd; + ssize_t n = read(STDIN_FILENO, &cmd, 1); + if (n > 0) { + if (cmd == 'k' || cmd == 'K') { + std::cerr << "⌨ Keyframe request received\n"; + encoder.forceKeyframe(); + } + // Ignore other characters and newlines + } + + usleep(10000); // 10ms - responsive to commands + } + + // Cleanup + std::cerr << "Stopping...\n"; + camera.stop(); + + return 0; +} diff --git a/livesrc/libcamera-bridge/v4l2_capture.cpp b/livesrc/libcamera-bridge/v4l2_capture.cpp new file mode 100644 index 00000000..432446cb --- /dev/null +++ b/livesrc/libcamera-bridge/v4l2_capture.cpp @@ -0,0 +1,315 @@ +#include "v4l2_capture.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct V4L2CaptureImpl { + int fd = -1; + int width = 0; + int height = 0; + int fps = 0; + bool use_mjpeg = false; + std::atomic running{false}; + + V4L2FrameCallback callback = nullptr; + void* user_data = nullptr; + std::string error_msg; + std::atomic thread_alive{false}; + + struct Buffer { + void* start; + size_t length; + }; + std::vector buffers; + + // Pre-allocated YUV420P conversion buffer + std::vector yuv420_buf; + + std::thread capture_thread; + + void capture_loop(); + bool is_healthy() const { return running.load() && thread_alive.load(); } + void yuyv_to_yuv420p(const uint8_t* src, uint8_t* dst, int w, int h); +}; + +// --- YUYV to YUV420P conversion --- +// YUYV: 2 pixels = 4 bytes [Y0 U0 Y1 V0] +// YUV420P: Y plane (w*h) + U plane (w*h/4) + V plane (w*h/4) +void V4L2CaptureImpl::yuyv_to_yuv420p(const uint8_t* src, uint8_t* dst, int w, int h) { + uint8_t* y_plane = dst; + uint8_t* u_plane = dst + w * h; + uint8_t* v_plane = dst + w * h + (w * h / 4); + + for (int row = 0; row < h; row++) { + const uint8_t* row_src = src + row * w * 2; + uint8_t* y_row = y_plane + row * w; + + for (int col = 0; col < w; col += 2) { + int idx = col * 2; + y_row[col] = row_src[idx + 0]; // Y0 + y_row[col + 1] = row_src[idx + 2]; // Y1 + + // Subsample U and V: every 2x2 block shares one U and V + if (row % 2 == 0) { + int uv_col = col / 2; + int uv_row = row / 2; + u_plane[uv_row * (w / 2) + uv_col] = row_src[idx + 1]; // U + v_plane[uv_row * (w / 2) + uv_col] = row_src[idx + 3]; // V + } + } + } +} + +void V4L2CaptureImpl::capture_loop() { + fprintf(stderr, "[V4L2Capture] Capture thread started\n"); + thread_alive.store(true); + + while (running.load()) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(fd, &fds); + + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + + int r = select(fd + 1, &fds, NULL, NULL, &tv); + if (r <= 0) continue; + + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { + if (errno == EAGAIN) continue; + fprintf(stderr, "[V4L2Capture] DQBUF failed: %s\n", strerror(errno)); + break; + } + + uint64_t ts_us = (uint64_t)buf.timestamp.tv_sec * 1000000 + buf.timestamp.tv_usec; + + if (callback) { + if (!use_mjpeg) { + // YUYV → YUV420P conversion + yuyv_to_yuv420p( + static_cast(buffers[buf.index].start), + yuv420_buf.data(), width, height + ); + callback(yuv420_buf.data(), yuv420_buf.size(), ts_us, user_data); + } else { + // MJPEG: pass raw (not supported by our encoder, reserved for future) + callback( + static_cast(buffers[buf.index].start), + buf.bytesused, ts_us, user_data + ); + } + } + + // Re-queue buffer + if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { + fprintf(stderr, "[V4L2Capture] QBUF failed: %s\n", strerror(errno)); + } + } + + thread_alive.store(false); + fprintf(stderr, "[V4L2Capture] Capture thread stopped\n"); +} + +// --- C API Implementation --- +extern "C" { + +V4L2CaptureHandle v4l2cap_create() { + return new V4L2CaptureImpl(); +} + +void v4l2cap_destroy(V4L2CaptureHandle handle) { + if (!handle) return; + auto* impl = static_cast(handle); + v4l2cap_stop(handle); + delete impl; +} + +bool v4l2cap_init(V4L2CaptureHandle handle, const V4L2CaptureParams* params) { + if (!handle || !params) return false; + auto* impl = static_cast(handle); + + impl->width = params->width; + impl->height = params->height; + impl->fps = params->fps; + impl->use_mjpeg = (params->input_format == 1); + + const char* dev = params->device ? params->device : "/dev/video2"; + fprintf(stderr, "[V4L2Capture] Opening %s (%dx%d @ %dfps, format=%s)\n", + dev, impl->width, impl->height, impl->fps, + impl->use_mjpeg ? "MJPEG" : "YUYV"); + + impl->fd = open(dev, O_RDWR); + if (impl->fd < 0) { + impl->error_msg = std::string("Failed to open ") + dev + ": " + strerror(errno); + fprintf(stderr, "[V4L2Capture] %s\n", impl->error_msg.c_str()); + return false; + } + + // Check capabilities + struct v4l2_capability cap = {}; + if (ioctl(impl->fd, VIDIOC_QUERYCAP, &cap) < 0) { + impl->error_msg = "QUERYCAP failed: " + std::string(strerror(errno)); + close(impl->fd); impl->fd = -1; return false; + } + + uint32_t caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? cap.device_caps : cap.capabilities; + if (!(caps & (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE))) { + impl->error_msg = "Device does not support video capture (caps=" + std::to_string(caps) + ")"; + close(impl->fd); impl->fd = -1; return false; + } + + // Set format + struct v4l2_format fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + // Attempt G_FMT first + if (ioctl(impl->fd, VIDIOC_G_FMT, &fmt) < 0) { + fprintf(stderr, "[V4L2Capture] Warning: G_FMT failed, using defaults\n"); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + } + + fmt.fmt.pix.width = impl->width; + fmt.fmt.pix.height = impl->height; + fmt.fmt.pix.pixelformat = impl->use_mjpeg ? V4L2_PIX_FMT_MJPEG : V4L2_PIX_FMT_YUYV; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + + if (ioctl(impl->fd, VIDIOC_S_FMT, &fmt) < 0) { + impl->error_msg = std::string("S_FMT failed: ") + strerror(errno); + fprintf(stderr, "[V4L2Capture] %s\n", impl->error_msg.c_str()); + close(impl->fd); + impl->fd = -1; + return false; + } + + // Actual negotiated size might differ + impl->width = fmt.fmt.pix.width; + impl->height = fmt.fmt.pix.height; + fprintf(stderr, "[V4L2Capture] Negotiated: %dx%d\n", impl->width, impl->height); + + // Set frame rate + struct v4l2_streamparm parm = {}; + parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + parm.parm.capture.timeperframe.numerator = 1; + parm.parm.capture.timeperframe.denominator = impl->fps; + ioctl(impl->fd, VIDIOC_S_PARM, &parm); + + // Allocate YUV420P conversion buffer + impl->yuv420_buf.resize(impl->width * impl->height * 3 / 2); + + // Request buffers - Increased to 16 for better jitter tolerance + struct v4l2_requestbuffers req = {}; + req.count = 16; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (ioctl(impl->fd, VIDIOC_REQBUFS, &req) < 0) { + impl->error_msg = std::string("REQBUFS failed: ") + strerror(errno); + close(impl->fd); + impl->fd = -1; + return false; + } + + for (unsigned int i = 0; i < req.count; i++) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (ioctl(impl->fd, VIDIOC_QUERYBUF, &buf) < 0) { + impl->error_msg = "QUERYBUF failed"; + return false; + } + + void* start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, impl->fd, buf.m.offset); + if (start == MAP_FAILED) { + impl->error_msg = "mmap failed"; + return false; + } + impl->buffers.push_back({start, buf.length}); + } + + fprintf(stderr, "[V4L2Capture] Init OK, %zu buffers allocated\n", impl->buffers.size()); + return true; +} + +bool v4l2cap_start(V4L2CaptureHandle handle) { + if (!handle) return false; + auto* impl = static_cast(handle); + + // Queue all buffers + for (unsigned int i = 0; i < impl->buffers.size(); i++) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + if (ioctl(impl->fd, VIDIOC_QBUF, &buf) < 0) return false; + } + + // Stream ON + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(impl->fd, VIDIOC_STREAMON, &type) < 0) { + impl->error_msg = std::string("STREAMON failed: ") + strerror(errno); + return false; + } + + impl->running.store(true); + impl->capture_thread = std::thread(&V4L2CaptureImpl::capture_loop, impl); + fprintf(stderr, "[V4L2Capture] Streaming started\n"); + return true; +} + +void v4l2cap_stop(V4L2CaptureHandle handle) { + if (!handle) return; + auto* impl = static_cast(handle); + + impl->running.store(false); + if (impl->capture_thread.joinable()) { + impl->capture_thread.join(); + } + + if (impl->fd >= 0) { + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + ioctl(impl->fd, VIDIOC_STREAMOFF, &type); + + for (auto& buf : impl->buffers) { + if (buf.start && buf.start != MAP_FAILED) munmap(buf.start, buf.length); + } + impl->buffers.clear(); + + close(impl->fd); + impl->fd = -1; + } +} + +void v4l2cap_set_callback(V4L2CaptureHandle handle, V4L2FrameCallback callback, void* user_data) { + if (!handle) return; + auto* impl = static_cast(handle); + impl->callback = callback; + impl->user_data = user_data; +} + +bool v4l2cap_is_running(V4L2CaptureHandle handle) { + if (!handle) return false; + return static_cast(handle)->is_healthy(); +} + +const char* v4l2cap_get_error(V4L2CaptureHandle handle) { + if (!handle) return "NULL handle"; + return static_cast(handle)->error_msg.c_str(); +} + +} // extern "C" diff --git a/livesrc/libcamera-bridge/v4l2_capture.h b/livesrc/libcamera-bridge/v4l2_capture.h new file mode 100644 index 00000000..1fd305cf --- /dev/null +++ b/livesrc/libcamera-bridge/v4l2_capture.h @@ -0,0 +1,42 @@ +#ifndef V4L2_CAPTURE_H +#define V4L2_CAPTURE_H + +#include +#include +#include + +struct V4L2CaptureParams { + const char* device; // e.g. "/dev/video2" + int width; + int height; + int fps; + int input_format; // 0 = YUYV (auto-convert to YUV420P), 1 = MJPEG +}; + +// Callback: delivers YUV420P frames ready for the encoder (CPU path) +typedef void (*V4L2FrameCallback)(const uint8_t* data, size_t size, uint64_t timestamp_us, void* user_data); + +// Callback: delivers DMA-BUF file descriptor (Zero-copy path for RDK) +typedef void (*V4L2FDFrameCallback)(int dma_fd, size_t size, uint64_t timestamp_us, void* user_data); + +typedef void* V4L2CaptureHandle; + +#ifdef __cplusplus +extern "C" { +#endif + +V4L2CaptureHandle v4l2cap_create(); +void v4l2cap_destroy(V4L2CaptureHandle handle); +bool v4l2cap_init(V4L2CaptureHandle handle, const V4L2CaptureParams* params); +bool v4l2cap_start(V4L2CaptureHandle handle); +void v4l2cap_stop(V4L2CaptureHandle handle); +void v4l2cap_set_callback(V4L2CaptureHandle handle, V4L2FrameCallback callback, void* user_data); +void v4l2cap_set_fd_callback(V4L2CaptureHandle handle, V4L2FDFrameCallback callback, void* user_data); +bool v4l2cap_is_running(V4L2CaptureHandle handle); +const char* v4l2cap_get_error(V4L2CaptureHandle handle); + +#ifdef __cplusplus +} +#endif + +#endif // V4L2_CAPTURE_H diff --git a/livesrc/libcamera-bridge/v4l2_capture_rdk.cpp b/livesrc/libcamera-bridge/v4l2_capture_rdk.cpp new file mode 100644 index 00000000..69255536 --- /dev/null +++ b/livesrc/libcamera-bridge/v4l2_capture_rdk.cpp @@ -0,0 +1,193 @@ +#include "v4l2_capture.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_COUNT 16 + +struct Buffer { + void* start; + size_t length; + int dbuf_fd; // The DMA-BUF File Descriptor +}; + +class V4L2CaptureImpl { +public: + int fd = -1; + std::string error_msg; + uint32_t width, height, fps; + std::vector buffers; + std::thread cap_thread; + std::atomic running{false}; + + V4L2FrameCallback callback = nullptr; + V4L2FDFrameCallback fd_callback = nullptr; + void* user_data = nullptr; + + ~V4L2CaptureImpl() { + stop(); + if (fd >= 0) close(fd); + } + + void stop() { + running = false; + if (cap_thread.joinable()) cap_thread.join(); + + if (fd >= 0) { + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + ioctl(fd, VIDIOC_STREAMOFF, &type); + + for (auto& buf : buffers) { + if (buf.start) munmap(buf.start, buf.length); + if (buf.dbuf_fd >= 0) close(buf.dbuf_fd); + } + buffers.clear(); + } + } +}; + +extern "C" { + +V4L2CaptureHandle v4l2cap_create() { + return new V4L2CaptureImpl(); +} + +void v4l2cap_destroy(V4L2CaptureHandle handle) { + delete static_cast(handle); +} + +bool v4l2cap_init(V4L2CaptureHandle handle, const V4L2CaptureParams* params) { + auto* impl = static_cast(handle); + impl->fd = open(params->device, O_RDWR); + if (impl->fd < 0) { + impl->error_msg = "Failed to open device: " + std::string(strerror(errno)); + return false; + } + + impl->width = params->width; + impl->height = params->height; + impl->fps = params->fps; + + // 1. Set Format + struct v4l2_format fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + fmt.fmt.pix.width = params->width; + fmt.fmt.pix.height = params->height; + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // Typically USB cameras + fmt.fmt.pix.field = V4L2_FIELD_ANY; + + if (ioctl(impl->fd, VIDIOC_S_FMT, &fmt) < 0) { + impl->error_msg = "S_FMT failed"; + return false; + } + + // 2. Request Buffers + struct v4l2_requestbuffers req = {}; + req.count = BUFFER_COUNT; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (ioctl(impl->fd, VIDIOC_REQBUFS, &req) < 0) { + impl->error_msg = "REQBUFS failed"; + return false; + } + + // 3. Map Buffers & EXPORT DMA-BUF FDs + for (uint32_t i = 0; i < req.count; ++i) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (ioctl(impl->fd, VIDIOC_QUERYBUF, &buf) < 0) return false; + + Buffer b; + b.length = buf.length; + b.start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, impl->fd, buf.m.offset); + + // --- THE ZERO-COPY KEY: Export FD --- + struct v4l2_exportbuffer expbuf = {}; + expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + expbuf.index = i; + if (ioctl(impl->fd, VIDIOC_EXPBUF, &expbuf) < 0) { + b.dbuf_fd = -1; // Fallback to copy if export fails + } else { + b.dbuf_fd = expbuf.fd; + } + + impl->buffers.push_back(b); + ioctl(impl->fd, VIDIOC_QBUF, &buf); + } + + return true; +} + +static void capture_loop(V4L2CaptureImpl* impl) { + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + ioctl(impl->fd, VIDIOC_STREAMON, &type); + + while (impl->running) { + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (ioctl(impl->fd, VIDIOC_DQBUF, &buf) < 0) continue; + + uint64_t ts = (uint64_t)buf.timestamp.tv_sec * 1000000 + buf.timestamp.tv_usec; + + // Path A: Low Latency / Zero Copy (RDK Path) + if (impl->fd_callback && impl->buffers[buf.index].dbuf_fd >= 0) { + impl->fd_callback(impl->buffers[buf.index].dbuf_fd, buf.bytesused, ts, impl->user_data); + } + // Path B: Legacy CPU Copy (Fallback) + else if (impl->callback) { + impl->callback((uint8_t*)impl->buffers[buf.index].start, buf.bytesused, ts, impl->user_data); + } + + ioctl(impl->fd, VIDIOC_QBUF, &buf); + } +} + +bool v4l2cap_start(V4L2CaptureHandle handle) { + auto* impl = static_cast(handle); + if (impl->running) return true; + impl->running = true; + impl->cap_thread = std::thread(capture_loop, impl); + return true; +} + +void v4l2cap_set_callback(V4L2CaptureHandle handle, V4L2FrameCallback callback, void* user_data) { + auto* impl = static_cast(handle); + impl->callback = callback; + impl->user_data = user_data; +} + +void v4l2cap_set_fd_callback(V4L2CaptureHandle handle, V4L2FDFrameCallback callback, void* user_data) { + auto* impl = static_cast(handle); + impl->fd_callback = callback; + impl->user_data = user_data; +} + +void v4l2cap_stop(V4L2CaptureHandle handle) { + static_cast(handle)->stop(); +} + +bool v4l2cap_is_running(V4L2CaptureHandle handle) { + return static_cast(handle)->running; +} + +const char* v4l2cap_get_error(V4L2CaptureHandle handle) { + return static_cast(handle)->error_msg.c_str(); +} + +} // extern "C" diff --git a/livesrc/src/bin/livesrc.rs b/livesrc/src/bin/livesrc.rs new file mode 100644 index 00000000..6f945082 --- /dev/null +++ b/livesrc/src/bin/livesrc.rs @@ -0,0 +1,90 @@ +use clap::Parser; +use std::sync::{Arc, RwLock}; +use tracing::{debug, info, warn}; + +use livesrc::config::Config; + +#[derive(Parser)] +#[command(name = "livesrc", version, about = "Lightweight camera source with WHEP streaming")] +struct Args { + /// Path to config file + #[arg(short, long)] + config: Option, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let cfg: Config = livesrc::utils::load("livesrc", args.config); + if let Err(e) = cfg.validate() { + eprintln!("Config validation failed: {}", e); + std::process::exit(1); + } + + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter(format!( + "livesrc={},tower_http=info,webrtc=error", + cfg.log.level + )) + .init(); + + warn!("set log level: {}", cfg.log.level); + debug!("load config: {:?}", cfg); + + info!("livesrc starting..."); + if let Some(stream) = &cfg.stream { + info!(" Stream ID: {}", stream.id); + info!(" RTP Port: {}", stream.rtp_port); + } + if let Some(camera) = &cfg.camera { + info!(" Camera: {}", camera.device); + } + info!(" Listen: {}", cfg.http.listen); + + let listener = match tokio::net::TcpListener::bind(&cfg.http.listen).await { + Ok(l) => l, + Err(e) => { + tracing::error!("Failed to bind to {}: {}", cfg.http.listen, e); + std::process::exit(1); + } + }; + + let config = Arc::new(RwLock::new(cfg)); + + if let Err(e) = livesrc::serve(config, listener, shutdown_signal()).await { + tracing::error!("Server error: {}", e); + std::process::exit(1); + } + + info!("livesrc shutdown"); +} + +async fn shutdown_signal() { + use tokio::signal; + + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + info!("Shutdown signal received"); +} diff --git a/livesrc/src/config.rs b/livesrc/src/config.rs new file mode 100644 index 00000000..6d803d1d --- /dev/null +++ b/livesrc/src/config.rs @@ -0,0 +1,492 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// ============================================================ +// 新的 Paths 配置结构 +// ============================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub http: HttpConfig, + pub log: LogConfig, + #[serde(default)] + pub webrtc: WebRtcConfig, + + // ============ 新增:Paths 配置 ============ + #[serde(default)] + pub path_defaults: PathConfig, + + #[serde(default)] + pub paths: HashMap, + + // ============ 向后兼容:旧配置 ============ + #[serde(skip_serializing_if = "Option::is_none")] + pub stream: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub camera: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub whip: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PathConfig { + /// 数据源类型 + #[serde(default)] + pub source: SourceType, + + /// 按需启动 + #[serde(default = "default_true")] + pub source_on_demand: bool, + + /// 最大订阅者数 (0 = 无限) + #[serde(default)] + pub max_readers: usize, + + /// RTP 端口(某些源需要) + #[serde(skip_serializing_if = "Option::is_none")] + pub rtp_port: Option, + + /// RTP 目标地址(默认为 127.0.0.1) + #[serde(skip_serializing_if = "Option::is_none")] + pub rtp_dest: Option, + + /// Codec 配置 + #[serde(default)] + pub codec: CodecConfig, + + /// Libcamera 配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub libcamera: Option, + + /// V4L2 配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub v4l2: Option, + + /// WHIP 配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub whip: Option, + + /// RTSP 配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub rtsp: Option, +} + +fn default_true() -> bool { + true +} + +impl Default for PathConfig { + fn default() -> Self { + Self { + source: SourceType::default(), + source_on_demand: true, + max_readers: 0, + rtp_port: None, + rtp_dest: None, + codec: CodecConfig::default(), + libcamera: None, + v4l2: None, + whip: None, + rtsp: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SourceType { + /// 等待 WHEP 推流 + Publisher, + + /// libcamera(树莓派摄像头)via custom libcamera-bridge + Libcamera, + + /// rpicam-vid(树莓派官方工具,自动硬件编码) + Rpicam, + + /// V4L2 直接捕获 + V4l2, + + /// WHIP 推流到其他服务器 + Whip, + + /// RTSP URL 拉流 + Rtsp(String), + + /// 本地文件 + File(String), +} + +impl Default for SourceType { + fn default() -> Self { + SourceType::Publisher + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibcameraConfig { + #[serde(default = "default_width")] + pub width: u32, + + #[serde(default = "default_height")] + pub height: u32, + + #[serde(default = "default_fps")] + pub fps: u32, + + #[serde(default = "default_bitrate")] + pub bitrate: u32, + + #[serde(default = "default_codec_str")] + pub codec: String, + + #[serde(default)] + pub camera_id: u32, + + #[serde(default)] + pub rotation: u32, + + #[serde(default)] + pub hflip: bool, + + #[serde(default)] + pub vflip: bool, +} + +fn default_width() -> u32 { 1920 } +fn default_height() -> u32 { 1080 } +fn default_fps() -> u32 { 30 } +fn default_bitrate() -> u32 { 2_000_000 } +fn default_codec_str() -> String { "h264".to_string() } + +impl Default for LibcameraConfig { + fn default() -> Self { + Self { + width: 1920, + height: 1080, + fps: 30, + bitrate: 2_000_000, + codec: "h264".to_string(), + camera_id: 0, + rotation: 0, + hflip: false, + vflip: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RtspConfig { + #[serde(default = "default_rtsp_transport")] + pub transport: String, + + #[serde(default = "default_timeout")] + pub timeout: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub buffer_size: Option, +} + +fn default_rtsp_transport() -> String { "tcp".to_string() } +fn default_timeout() -> u32 { 10 } + +impl Default for RtspConfig { + fn default() -> Self { + Self { + transport: "tcp".to_string(), + timeout: 10, + buffer_size: None, + } + } +} + +// ============================================================ +// 旧配置结构(向后兼容) +// ============================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HttpConfig { + pub listen: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogConfig { + pub level: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StreamConfig { + pub id: String, + pub rtp_port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CaptureSource { + Ffmpeg, + V4l2, +} + +impl Default for CaptureSource { + fn default() -> Self { + CaptureSource::Ffmpeg + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct V4l2Config { + pub device: String, + pub width: u32, + pub height: u32, + pub fps: u32, + pub format: String, + #[serde(default)] + pub bitrate: Option, +} + +impl Default for V4l2Config { + fn default() -> Self { + Self { + device: "/dev/video0".to_string(), + width: 640, + height: 480, + fps: 30, + format: "H264".to_string(), + bitrate: Some(1_000_000), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CameraConfig { + pub device: String, + #[serde(default)] + pub source: CaptureSource, + #[serde(default)] + pub command: Option, + pub codec: CodecConfig, + #[serde(default)] + pub v4l2: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CodecConfig { + pub mime_type: String, + pub clock_rate: u32, + #[serde(default)] + pub channels: u16, + #[serde(default)] + pub sdp_fmtp_line: Option, +} + +impl Default for CodecConfig { + fn default() -> Self { + Self { + mime_type: "video/H264".to_string(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: Some( + "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f" + .to_string(), + ), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WebRtcConfig { + #[serde(default)] + pub ice_servers: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + #[default] + Whep, + Whip, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WhipConfig { + pub url: String, + pub token: Option, +} + +// ============================================================ +// 配置验证和辅助方法 +// ============================================================ + +impl Config { + pub fn validate(&self) -> anyhow::Result<()> { + // 新配置验证 + if !self.paths.is_empty() { + for (name, path_config) in &self.paths { + self.validate_path(name, path_config)?; + } + return Ok(()); + } + + // 旧配置验证(向后兼容) + if let Some(stream) = &self.stream { + if stream.id.is_empty() { + anyhow::bail!("stream.id cannot be empty"); + } + if stream.rtp_port == 0 { + anyhow::bail!("stream.rtp_port must be greater than 0"); + } + } + + if let Some(mode) = &self.mode { + if *mode == Mode::Whip && self.whip.is_none() { + anyhow::bail!("WHIP mode requires [whip] configuration"); + } + } + + if let Some(camera) = &self.camera { + match camera.source { + CaptureSource::Ffmpeg => { + if camera.command.is_none() || camera.command.as_ref().unwrap().is_empty() { + anyhow::bail!("camera.command cannot be empty when source is ffmpeg"); + } + } + CaptureSource::V4l2 => { + if camera.v4l2.is_none() { + anyhow::bail!("camera.v4l2 config is required when source is v4l2"); + } + } + } + } + + Ok(()) + } + + fn validate_path(&self, name: &str, config: &PathConfig) -> anyhow::Result<()> { + if name.is_empty() { + anyhow::bail!("Path name cannot be empty"); + } + + match &config.source { + SourceType::Libcamera | SourceType::Rpicam => { + if config.libcamera.is_none() { + anyhow::bail!( + "Path '{}': libcamera/rpicam source requires [paths.{}.libcamera] config", + name, name + ); + } + if config.rtp_port.is_none() { + anyhow::bail!("Path '{}': libcamera/rpicam source requires rtp_port", name); + } + } + SourceType::V4l2 => { + if config.v4l2.is_none() { + anyhow::bail!( + "Path '{}': v4l2 source requires [paths.{}.v4l2] config", + name, name + ); + } + if config.rtp_port.is_none() { + anyhow::bail!("Path '{}': v4l2 source requires rtp_port", name); + } + } + SourceType::Whip => { + if config.whip.is_none() { + anyhow::bail!( + "Path '{}': whip source requires [paths.{}.whip] config", + name, name + ); + } + } + SourceType::Rtsp(url) => { + if url.is_empty() { + anyhow::bail!("Path '{}': rtsp URL cannot be empty", name); + } + if !url.starts_with("rtsp://") && !url.starts_with("rtsps://") { + anyhow::bail!("Path '{}': invalid RTSP URL: {}", name, url); + } + } + SourceType::File(path) => { + if path.is_empty() { + anyhow::bail!("Path '{}': file path cannot be empty", name); + } + } + SourceType::Publisher => { + // Publisher 不需要额外配置 + } + } + + Ok(()) + } + + /// 检查是否为新配置格式 + pub fn is_paths_mode(&self) -> bool { + !self.paths.is_empty() + } +} + +impl Default for Config { + fn default() -> Self { + Self { + http: HttpConfig { + listen: "0.0.0.0:7778".to_string(), + }, + log: LogConfig { + level: "info".to_string(), + }, + webrtc: WebRtcConfig::default(), + path_defaults: PathConfig::default(), + paths: HashMap::new(), + // 旧配置默认值 + stream: Some(StreamConfig { + id: "camera".to_string(), + rtp_port: 5004, + }), + camera: Some(CameraConfig { + device: "/dev/video0".to_string(), + source: CaptureSource::Ffmpeg, + command: Some( + "ffmpeg -f v4l2 -video_size 640x480 -framerate 30 -i /dev/video0 -pix_fmt yuv420p -c:v libx264 -preset ultrafast -tune zerolatency -profile:v baseline -g 30 -b:v 1M -f rtp rtp://127.0.0.1:5004" + .to_string(), + ), + codec: CodecConfig::default(), + v4l2: None, + }), + mode: Some(Mode::default()), + whip: None, + } + } +} + +impl From for webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability { + fn from(val: CodecConfig) -> Self { + webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability { + mime_type: val.mime_type, + clock_rate: val.clock_rate, + channels: val.channels, + sdp_fmtp_line: val.sdp_fmtp_line.unwrap_or_default(), + rtcp_feedback: vec![ + webrtc::rtp_transceiver::RTCPFeedback { + typ: "goog-remb".to_owned(), + parameter: "".to_owned(), + }, + webrtc::rtp_transceiver::RTCPFeedback { + typ: "ccm".to_owned(), + parameter: "fir".to_owned(), + }, + webrtc::rtp_transceiver::RTCPFeedback { + typ: "nack".to_owned(), + parameter: "".to_owned(), + }, + webrtc::rtp_transceiver::RTCPFeedback { + typ: "nack".to_owned(), + parameter: "pli".to_owned(), + }, + ], + } + } +} diff --git a/livesrc/src/lib.rs b/livesrc/src/lib.rs new file mode 100644 index 00000000..1e7d1b45 --- /dev/null +++ b/livesrc/src/lib.rs @@ -0,0 +1,431 @@ +pub mod config; +pub mod utils; +pub mod whep_handler; // Keep for testing +pub mod rtp; // New: RTP output module + +#[cfg(feature = "v4l2")] +pub mod v4l2_capture; + +pub mod path_manager; +pub mod sources; + +use axum::http::header; +use axum::Router; +use std::future::Future; +use std::sync::{Arc, Mutex, RwLock}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tower_http::cors::{Any, CorsLayer}; +use tower_http::trace::TraceLayer; +use tracing::{debug, error, info, warn}; +use webrtc::api::media_engine::MediaEngine; +use webrtc::api::setting_engine::SettingEngine; +use webrtc::api::{APIBuilder, API}; +use webrtc::interceptor::registry::Registry; +use webrtc::peer_connection::RTCPeerConnection; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; + + +use config::{CaptureSource, Config, Mode}; + +/// Stream state for StartOnDemand mode +struct StreamState { + subscriber_count: usize, + track: Arc, + rtp_receiver_handle: Option>, + shutdown_tx: Option>, + + // FFmpeg mode + child_process: Option>, + + // V4L2 mode + #[cfg(feature = "v4l2")] + v4l2_handle: Option>, +} + +/// LiveSrc Manager - handles single camera stream with StartOnDemand +#[derive(Clone)] +pub struct LiveSrcManager { + stream_id: String, + state: Arc>, + whep_session: Arc>>>, + pub webrtc_api: Arc, + pub config: Arc>, + pub path_manager: Arc, +} + +impl LiveSrcManager { + pub fn new(config: Arc>, webrtc_api: Arc) -> Self { + let cfg = config.read().unwrap(); + + // 检查是否为新的 paths 模式 + let is_paths_mode = !cfg.paths.is_empty(); + + // Legacy mode: 需要 stream 和 camera 配置 + // Paths mode: 不需要,使用默认值 + let (stream_id, track) = if is_paths_mode { + // Paths 模式 - 使用占位值(旧的 LiveSrcManager 不会被使用) + let default_track = Arc::new(TrackLocalStaticRTP::new( + cfg.path_defaults.codec.clone().into(), + "legacy".to_string(), + "livesrc-stream".to_owned(), + )); + ("legacy".to_string(), default_track) + } else { + // Legacy 模式 - 需要完整配置 + let stream_id = cfg.stream.as_ref().expect("stream config required in legacy mode").id.clone(); + let track = Arc::new(TrackLocalStaticRTP::new( + cfg.camera.as_ref().expect("camera config required in legacy mode").codec.clone().into(), + stream_id.clone(), + "livesrc-stream".to_owned(), + )); + (stream_id, track) + }; + + drop(cfg); + + let state = StreamState { + subscriber_count: 0, + track, + rtp_receiver_handle: None, + shutdown_tx: None, + child_process: None, + #[cfg(feature = "v4l2")] + v4l2_handle: None, + }; + + let path_manager = Arc::new(path_manager::PathManager::new(config.clone(), webrtc_api.clone())); + + Self { + stream_id, + state: Arc::new(Mutex::new(state)), + whep_session: Arc::new(Mutex::new(None)), + webrtc_api, + config, + path_manager, + } + } + + /// Add a subscriber - starts camera source on first subscriber (StartOnDemand) + pub fn add_subscriber(&self) -> Option> { + let mut state = self.state.lock().unwrap(); + state.subscriber_count += 1; + + info!( + stream_id = %self.stream_id, + subscribers = state.subscriber_count, + "Subscriber added" + ); + + // Start on first subscriber (StartOnDemand mode) + if state.subscriber_count == 1 { + info!( + stream_id = %self.stream_id, + "First subscriber, starting camera source" + ); + + let config = self.config.read().unwrap(); + let camera = config.camera.as_ref().expect("camera config required"); + let stream = config.stream.as_ref().expect("stream config required"); + let capture_source = camera.source.clone(); + let rtp_port = stream.rtp_port; + + match capture_source { + config::CaptureSource::Ffmpeg => { + let command = camera.command.clone().unwrap_or_default(); + drop(config); + + // Start FFmpeg encoder + match cli::create_child(Some(command)) { + Ok(Some(child_guard)) => { + state.child_process = Some(Arc::new(child_guard)); + info!(stream_id = %self.stream_id, "FFmpeg encoder started"); + } + Ok(None) => { + warn!(stream_id = %self.stream_id, "No command provided"); + } + Err(e) => { + error!(stream_id = %self.stream_id, error = %e, "Failed to start FFmpeg"); + } + } + + // TODO: Replace with new RTP output module + // Old RTP receiver code removed - will be replaced with libcamera-bridge integration + warn!(stream_id = %self.stream_id, "FFmpeg mode needs RTP integration update"); + } + + #[cfg(feature = "v4l2")] + config::CaptureSource::V4l2 => { + let device = camera.device.clone(); + let v4l2_config = camera.v4l2.clone().expect("v4l2 config required"); + drop(config); + + info!(stream_id = %self.stream_id, "Starting V4L2 capture mode"); + + // Create V4L2 capture + match v4l2_capture::V4l2Capture::new(&device, v4l2_config) { + Ok(mut capture) => { + if let Err(e) = capture.start() { + error!(stream_id = %self.stream_id, error = %e, "Failed to start V4L2 capture"); + return Some(state.track.clone()); + } + + info!(stream_id = %self.stream_id, "V4L2 capture started"); + + // Create channels for frame data and shutdown + let (frame_tx, mut frame_rx) = mpsc::channel::>(10); + let (shutdown_tx, shutdown_rx) = mpsc::channel(1); + state.shutdown_tx = Some(shutdown_tx); + + // Spawn V4L2 capture loop + let v4l2_handle = tokio::task::spawn_blocking(move || { + let rt = tokio::runtime::Handle::current(); + rt.block_on(async { + if let Err(e) = capture.capture_loop(frame_tx, shutdown_rx).await { + error!("V4L2 capture loop error: {}", e); + } + }); + }); + state.v4l2_handle = Some(v4l2_handle); + + // Spawn RTP packetizer to process frames + let track_clone = state.track.clone(); + let rtp_handle = tokio::spawn(async move { + while let Some(frame_data) = frame_rx.recv().await { + // Send raw H264/H265 frame to RTP track + // TODO: Proper RTP packetization for H.264/H.265 + use bytes::Bytes; + let packet = webrtc::rtp::packet::Packet { + header: webrtc::rtp::header::Header { + version: 2, + padding: false, + extension: false, + marker: true, // Mark end of frame + payload_type: 96, // Dynamic payload type for H264 + sequence_number: 0, // Will be set by track + timestamp: 0, // Will be set by track + ssrc: 0, // Will be set by track + ..Default::default() + }, + payload: Bytes::from(frame_data), + }; + + if let Err(e) = track_clone.write_rtp(&packet).await { + error!("Failed to write RTP packet to track: {}", e); + break; + } + } + }); + state.rtp_receiver_handle = Some(rtp_handle); + } + Err(e) => { + error!(stream_id = %self.stream_id, error = %e, "Failed to create V4L2 capture"); + } + } + } + + #[cfg(not(feature = "v4l2"))] + config::CaptureSource::V4l2 => { + drop(config); + error!(stream_id = %self.stream_id, "V4L2 mode requested but v4l2 feature not enabled"); + } + } + } + + Some(state.track.clone()) + } + + /// Remove a subscriber - stops camera source when last subscriber leaves + pub fn remove_subscriber(&self) { + let mut state = self.state.lock().unwrap(); + + if state.subscriber_count > 0 { + state.subscriber_count -= 1; + info!( + stream_id = %self.stream_id, + subscribers = state.subscriber_count, + "Subscriber removed" + ); + } + + // Stop on last subscriber (StartOnDemand mode) + if state.subscriber_count == 0 { + info!( + stream_id = %self.stream_id, + "Last subscriber left, stopping camera source" + ); + + // Stop FFmpeg (ChildGuard kills process on drop) + if let Some(_child) = state.child_process.take() { + debug!(stream_id = %self.stream_id, "FFmpeg encoder stopped"); + } + + // Stop V4L2 capture + #[cfg(feature = "v4l2")] + if let Some(handle) = state.v4l2_handle.take() { + handle.abort(); + debug!(stream_id = %self.stream_id, "V4L2 capture stopped"); + } + + // Stop RTP receiver/packetizer + if let Some(tx) = state.shutdown_tx.take() { + let _ = tx.try_send(()); + } + if let Some(handle) = state.rtp_receiver_handle.take() { + handle.abort(); + } + } + } + + pub fn set_whep_session(&self, pc: Arc) { + let mut session = self.whep_session.lock().unwrap(); + *session = Some(pc); + } + + pub fn remove_whep_session(&self) { + let mut session = self.whep_session.lock().unwrap(); + *session = None; + } + + pub async fn shutdown(&self) { + // Close WHEP session + if let Some(pc) = self.whep_session.lock().unwrap().take() { + let _ = pc.close().await; + } + + // Stop stream + let mut state = self.state.lock().unwrap(); + if let Some(_child) = state.child_process.take() { + info!(stream_id = %self.stream_id, "FFmpeg stopped"); + } + + #[cfg(feature = "v4l2")] + if let Some(handle) = state.v4l2_handle.take() { + handle.abort(); + info!(stream_id = %self.stream_id, "V4L2 capture stopped"); + } + + if let Some(tx) = state.shutdown_tx.take() { + let _ = tx.try_send(()); + } + if let Some(handle) = state.rtp_receiver_handle.take() { + handle.abort(); + } + } +} + +pub async fn serve( + config: Arc>, + listener: TcpListener, + shutdown_signal: impl Future + Send + 'static, +) -> anyhow::Result<()> { + let mode = config.read().unwrap().mode.clone().unwrap_or_default(); // Main run loop based on mode + let result = match mode { + Mode::Whep => serve_whep(config, listener, shutdown_signal).await, + Mode::Whip => { + error!("WHIP mode is no longer supported - livesrc now outputs RTP to liveion"); + Err(anyhow::anyhow!("WHIP mode removed, please use WHEP mode for testing")) + } + }; + result +} + +async fn serve_whep( + config: Arc>, + listener: TcpListener, + shutdown_signal: impl Future + Send + 'static, +) -> anyhow::Result<()> { + // Create WebRTC API + let mut m = MediaEngine::default(); + m.register_default_codecs()?; + + let mut setting_engine = SettingEngine::default(); + setting_engine.set_ice_timeouts( + Some(std::time::Duration::from_secs(15)), + Some(std::time::Duration::from_secs(30)), + Some(std::time::Duration::from_secs(2)), + ); + + let registry = Registry::new(); + let api = APIBuilder::new() + .with_media_engine(m) + .with_setting_engine(setting_engine) + .with_interceptor_registry(registry) + .build(); + let webrtc_api = Arc::new(api); + + let manager = LiveSrcManager::new(config.clone(), webrtc_api); + + let app = Router::new() + .merge(whep_handler::create_router()) + .layer(TraceLayer::new_for_http()) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION]) + .expose_headers(vec![header::LOCATION, header::CONTENT_TYPE]), + ) + .with_state(manager.clone()); + + let addr = listener.local_addr()?; + info!("livesrc WHEP server listening on http://{}", addr); + info!("WHEP endpoint: http://{}/whep", addr); + + axum::serve(listener, app.into_make_service()) + .with_graceful_shutdown(shutdown_signal) + .await?; + + manager.shutdown().await; + info!("livesrc shutdown completed"); + + Ok(()) +} + +/* +// WHIP mode removed - livesrc now outputs RTP to liveion, not WHIP +// This function is preserved for reference but should not be used +pub async fn run_whip_mode(config: Arc>, shutdown_signal: impl Future) -> Result<()> { + let cfg = config.read().unwrap(); + let stream = cfg.stream.as_ref().expect("stream config required"); + let whip_config = cfg.whip.as_ref().expect("WHIP config required"); + let camera = cfg.camera.as_ref().expect("camera config required"); + + info!("Starting in WHIP mode"); + info!(" Stream ID: {}", stream.id); + info!(" RTP Port: {}", stream.rtp_port); + info!(" WHIP URL: {}", whip_config.url); + info!(" Camera: {}", camera.device); + + // Build FFmpeg command + let ffmpeg_command = match camera.source { + config::CaptureSource::Ffmpeg => { + camera.command.clone() + .ok_or_else(|| anyhow::anyhow!("FFmpeg command required"))? + } + config::CaptureSource::V4l2 => { + return Err(anyhow::anyhow!("V4L2 mode with WHIP not yet supported. Use FFmpeg mode.")); + } + }; + + let rtp_port = stream.rtp_port; + let whip_url = whip_config.url.clone(); + let token = whip_config.token.clone(); + let codec_config = camera.codec.clone(); + drop(cfg); + + // Create shutdown channel for WHIP client + let (shutdown_tx, shutdown_rx) = mpsc::channel(1); + + // Forward the shutdown signal + tokio::spawn(async move { + shutdown_signal.await; + let _ = shutdown_tx.send(()).await; + }); + + // NOTE: whip_client module has been removed + warn!("WHIP mode is no longer supported - livesrc outputs RTP to liveion instead"); + Err(anyhow::anyhow!("WHIP mode removed, please use WHEP mode for testing or integrate with liveion")) +} +*/ diff --git a/livesrc/src/path_manager.rs b/livesrc/src/path_manager.rs new file mode 100644 index 00000000..a369728f --- /dev/null +++ b/livesrc/src/path_manager.rs @@ -0,0 +1,188 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock, Mutex}; +use anyhow::Result; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::api::API; +use crate::config::{Config, PathConfig, SourceType}; +use crate::sources::{Source, LibcameraSource, RpicamSource, V4l2Source, WhipSource, RtspSource, PublisherSource}; + +/// Represents the runtime state of a single path. +struct PathEntry { + /// The source handler (ffmpeg, libcamera, etc.) + source: Box, + /// RTP track that will be offered to WebRTC peers. + track: Arc, + /// Number of current subscribers. + subscriber_count: usize, + /// Whether the source is currently running. + running: bool, + /// On‑demand flag from configuration. + source_on_demand: bool, + /// Maximum allowed subscribers (0 = unlimited). + max_readers: usize, +} + +/// Manages all configured paths. +pub struct PathManager { + /// Mapping from path name to its runtime entry (protected by Mutex for interior mutability). + paths: Arc>>, + /// Shared configuration (used for on‑demand start). + config: Arc>, + /// WebRTC API (passed to source implementations if needed). + webrtc_api: Arc, +} + +impl PathManager { + /// Build a new manager from the global configuration. + pub fn new(config: Arc>, webrtc_api: Arc) -> Self { + let cfg = config.read().unwrap(); + let mut paths = HashMap::new(); + // Apply defaults from `path_defaults` when a field is missing. + for (name, path_cfg) in &cfg.paths { + // Merge with defaults. + let merged = Self::merge_with_defaults(&cfg.path_defaults, path_cfg); + + // Create RTP track using the codec configuration. + let track = Arc::new(TrackLocalStaticRTP::new( + merged.codec.clone().into(), + name.clone(), + "livesrc-stream".to_owned(), + )); + + // Choose source implementation with proper parameters + let source: Box = match &merged.source { + SourceType::Libcamera => { + let libcamera_config = merged.libcamera.clone() + .expect("libcamera config required for Libcamera source"); + let rtp_port = merged.rtp_port + .expect("rtp_port required for Libcamera source"); + Box::new(LibcameraSource::new(libcamera_config, rtp_port, merged.rtp_dest.clone(), track.clone())) + } + SourceType::Rpicam => { + let rpicam_config = merged.libcamera.clone() + .expect("libcamera config required for Rpicam source (reuses same config structure)"); + let rtp_port = merged.rtp_port + .expect("rtp_port required for Rpicam source"); + Box::new(RpicamSource::new(rpicam_config, rtp_port, merged.rtp_dest.clone(), track.clone())) + } + SourceType::V4l2 => Box::new(V4l2Source), + SourceType::Whip => Box::new(WhipSource), + SourceType::Rtsp(_) => Box::new(RtspSource), + SourceType::Publisher => Box::new(PublisherSource), + SourceType::File(_) => Box::new(PublisherSource), // treat file as publisher for now + }; + + paths.insert( + name.clone(), + PathEntry { + source, + track, + subscriber_count: 0, + running: false, + source_on_demand: merged.source_on_demand, + max_readers: merged.max_readers, + }, + ); + } + drop(cfg); + + // Auto-start sources that are not on-demand + for (name, entry) in paths.iter_mut() { + if !entry.source_on_demand && !entry.running { + tracing::info!(path = %name, "Auto-starting source (source_on_demand = false)"); + match entry.source.start() { + Ok(_) => { + entry.running = true; + tracing::info!(path = %name, "Source auto-started successfully"); + } + Err(e) => { + tracing::error!(path = %name, error = %e, "Failed to auto-start source"); + } + } + } + } + + Self { + paths: Arc::new(Mutex::new(paths)), + config, + webrtc_api, + } + } + + /// Merge a specific path config with the global defaults. + fn merge_with_defaults(defaults: &PathConfig, specific: &PathConfig) -> PathConfig { + PathConfig { + source: specific.source.clone(), + source_on_demand: specific.source_on_demand, + max_readers: specific.max_readers, + rtp_port: specific.rtp_port.or(defaults.rtp_port), + rtp_dest: specific.rtp_dest.clone().or_else(|| defaults.rtp_dest.clone()), + codec: specific.codec.clone(), + libcamera: specific.libcamera.clone().or_else(|| defaults.libcamera.clone()), + v4l2: specific.v4l2.clone().or_else(|| defaults.v4l2.clone()), + whip: specific.whip.clone().or_else(|| defaults.whip.clone()), + rtsp: specific.rtsp.clone().or_else(|| defaults.rtsp.clone()), + } + } + + /// Add a subscriber to a given path. Starts the source on‑demand if required. + pub fn add_subscriber(&self, path_name: &str) -> Result>> { + tracing::debug!(path = %path_name, "add_subscriber called"); + + let mut paths = self.paths.lock().unwrap(); + let entry = paths.get_mut(path_name) + .ok_or_else(|| anyhow::anyhow!("Path '{}' not found", path_name))?; + + entry.subscriber_count += 1; + tracing::debug!(path = %path_name, count = entry.subscriber_count, "subscriber_count incremented"); + + // Enforce max_readers if set (>0). + if entry.max_readers > 0 && entry.subscriber_count > entry.max_readers { + entry.subscriber_count -= 1; // 回滚计数 + tracing::warn!(path = %path_name, limit = entry.max_readers, "max_readers limit exceeded"); + anyhow::bail!("Path '{}' exceeded max_readers limit ({})", path_name, entry.max_readers); + } + + // Start source on first subscriber when on‑demand. + if entry.subscriber_count == 1 && entry.source_on_demand { + tracing::info!(path = %path_name, "Starting source (on-demand, first subscriber)"); + entry.source.start()?; + entry.running = true; + tracing::info!(path = %path_name, "Source started successfully"); + } + + Ok(Some(entry.track.clone())) + } + + /// Remove a subscriber from a given path. Stops the source if on‑demand and no more subscribers. + pub fn remove_subscriber(&self, path_name: &str) -> Result<()> { + let mut paths = self.paths.lock().unwrap(); + let entry = paths.get_mut(path_name) + .ok_or_else(|| anyhow::anyhow!("Path '{}' not found", path_name))?; + + if entry.subscriber_count == 0 { + anyhow::bail!("Path '{}' has no subscribers to remove", path_name); + } + + entry.subscriber_count -= 1; + + // Stop source when last subscriber leaves (if on‑demand) + if entry.subscriber_count == 0 && entry.source_on_demand { + entry.stop()?; + } + + Ok(()) + } +} + +// Helper methods for PathEntry +impl PathEntry { + /// Stop the source if it is running. + fn stop(&mut self) -> Result<()> { + if self.running { + self.source.stop()?; + self.running = false; + } + Ok(()) + } +} diff --git a/livesrc/src/rtp/annex_b_parser.rs b/livesrc/src/rtp/annex_b_parser.rs new file mode 100644 index 00000000..5eb259f0 --- /dev/null +++ b/livesrc/src/rtp/annex_b_parser.rs @@ -0,0 +1,221 @@ +// H.264 Annex B format parser +// Finds start codes and extracts NAL units + +use anyhow::{Result, bail}; +use tokio::io::{AsyncRead, AsyncReadExt}; + +/// H.264 NAL unit type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NalType { + Slice = 1, + Dpa = 2, + Dpb = 3, + Dpc = 4, + Idr = 5, + Sei = 6, + Sps = 7, + Pps = 8, + Aud = 9, + EndSequence = 10, + EndStream = 11, + Filler = 12, + Unknown, +} + +impl From for NalType { + fn from(val: u8) -> Self { + match val & 0x1F { + 1 => NalType::Slice, + 2 => NalType::Dpa, + 3 => NalType::Dpb, + 4 => NalType::Dpc, + 5 => NalType::Idr, + 6 => NalType::Sei, + 7 => NalType::Sps, + 8 => NalType::Pps, + 9 => NalType::Aud, + 10 => NalType::EndSequence, + 11 => NalType::EndStream, + 12 => NalType::Filler, + _ => NalType::Unknown, + } + } +} + +/// Parsed NAL unit +#[derive(Debug, Clone)] +pub struct NalUnit { + pub nal_type: NalType, + pub data: Vec, // 包含 NAL header +} + +impl NalUnit { + pub fn is_keyframe(&self) -> bool { + matches!(self.nal_type, NalType::Idr | NalType::Sps | NalType::Pps) + } +} + +/// H.264 Annex B parser +pub struct AnnexBParser { + reader: R, + buffer: Vec, + position: usize, +} + +impl AnnexBParser { + pub fn new(reader: R) -> Self { + Self { + reader, + buffer: Vec::with_capacity(1024 * 1024), // 1MB buffer + position: 0, + } + } + + /// 读取下一个 NAL unit + pub async fn read_next_nal(&mut self) -> Result> { + loop { + // 查找起始码 + let start_pos = match self.find_start_code().await? { + Some(pos) => pos, + None => return Ok(None), // 真正到达 EOF + }; + + // 消费起始码之前的数据 + self.buffer.drain(..start_pos); + + // 跳过起始码本身 (3 或 4 字节) + let start_code_len = if self.buffer.len() >= 4 && &self.buffer[0..4] == &[0, 0, 0, 1] { + 4 + } else if self.buffer.len() >= 3 && &self.buffer[0..3] == &[0, 0, 1] { + 3 + } else { + bail!("Invalid start code at position 0"); + }; + + self.buffer.drain(..start_code_len); + + // 查找下一个起始码(确认 NAL 结束位置) + let end_pos = match self.find_start_code().await? { + Some(pos) => pos, + None => { + // 到流结尾了,提取剩下的所有数据作为一个 NAL + let remaining = self.buffer.len(); + if remaining == 0 { return Ok(None); } + remaining + } + }; + + if end_pos == 0 { + // 连续的起始码(空 NAL),跳过当前并继续查找下一个 + continue; + } + + // 提取 NAL unit 数据 + let nal_data: Vec = self.buffer.drain(..end_pos).collect(); + + // 解析 NAL type (NAL header 的第一个字节) + let nal_type = NalType::from(nal_data[0]); + + return Ok(Some(NalUnit { + nal_type, + data: nal_data, + })); + } + } + + /// 查找起始码(0x00 00 00 01 或 0x00 00 01) + async fn find_start_code(&mut self) -> Result> { + let mut empty_read_count = 0; + const MAX_INITIAL_RETRIES: u32 = 50; // Max retries for initial empty reads + + loop { + // 在已有 buffer 中查找 + if let Some(pos) = self.search_start_code() { + return Ok(Some(pos)); + } + + // 读取更多数据 + let mut chunk = vec![0u8; 8192]; + let n = self.reader.read(&mut chunk).await?; + + if n == 0 { + // Empty read - could be transient or true EOF + if self.buffer.is_empty() { + // No data in buffer yet - might be startup delay + empty_read_count += 1; + if empty_read_count > MAX_INITIAL_RETRIES { + // Truly no data after many retries + return Ok(None); + } + // Wait and retry + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + continue; + } else { + // Have some buffer data, this might be end of current NAL + return Ok(None); + } + } + + // Got data - reset retry counter + empty_read_count = 0; + self.buffer.extend_from_slice(&chunk[..n]); + } + } + + /// 在 buffer 中搜索起始码 + fn search_start_code(&self) -> Option { + if self.buffer.len() < 3 { + return None; + } + + for i in self.position..self.buffer.len() - 2 { + // 检查 0x00 00 01 + if self.buffer[i] == 0 + && self.buffer[i + 1] == 0 + && self.buffer[i + 2] == 1 { + return Some(i); + } + + // 检查 0x00 00 00 01 + if i < self.buffer.len() - 3 + && self.buffer[i] == 0 + && self.buffer[i + 1] == 0 + && self.buffer[i + 2] == 0 + && self.buffer[i + 3] == 1 { + return Some(i); + } + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::BufReader; + + #[tokio::test] + async fn test_parse_nal() { + // 测试数据: 两个 NAL units + let data = vec![ + 0x00, 0x00, 0x00, 0x01, // Start code + 0x67, 0x42, 0x00, 0x1e, // SPS NAL + 0x00, 0x00, 0x00, 0x01, // Start code + 0x68, 0xce, 0x38, 0x80, // PPS NAL + ]; + + let cursor = std::io::Cursor::new(data); + let mut parser = AnnexBParser::new(BufReader::new(cursor)); + + // 第一个 NAL (SPS) + let nal1 = parser.read_next_nal().await.unwrap().unwrap(); + assert_eq!(nal1.nal_type, NalType::Sps); + assert_eq!(nal1.data, vec![0x67, 0x42, 0x00, 0x1e]); + + // 第二个 NAL (PPS) + let nal2 = parser.read_next_nal().await.unwrap().unwrap(); + assert_eq!(nal2.nal_type, NalType::Pps); + assert_eq!(nal2.data, vec![0x68, 0xce, 0x38, 0x80]); + } +} diff --git a/livesrc/src/rtp/h264_packetizer.rs b/livesrc/src/rtp/h264_packetizer.rs new file mode 100644 index 00000000..14a66806 --- /dev/null +++ b/livesrc/src/rtp/h264_packetizer.rs @@ -0,0 +1,281 @@ +// RTP H.264 Packetizer (RFC 6184) +// Converts H.264 NAL units to RTP packets + +use anyhow::Result; + +use super::annex_b_parser::NalUnit; + +/// RTP Header structure +#[derive(Debug, Clone)] +pub struct RtpHeader { + pub version: u8, // Always 2 + pub padding: bool, + pub extension: bool, + pub marker: bool, // Last packet of frame + pub payload_type: u8, // 96 for H.264 + pub sequence: u16, + pub timestamp: u32, // 90kHz clock + pub ssrc: u32, // Synchronization source +} + +impl RtpHeader { + pub fn to_bytes(&self) -> [u8; 12] { + let mut bytes = [0u8; 12]; + + // Byte 0: V(2), P(1), X(1), CC(4) + bytes[0] = (self.version << 6) + | ((self.padding as u8) << 5) + | ((self.extension as u8) << 4); + + // Byte 1: M(1), PT(7) + bytes[1] = ((self.marker as u8) << 7) | (self.payload_type & 0x7F); + + // Bytes 2-3: Sequence number + bytes[2..4].copy_from_slice(&self.sequence.to_be_bytes()); + + // Bytes 4-7: Timestamp + bytes[4..8].copy_from_slice(&self.timestamp.to_be_bytes()); + + // Bytes 8-11: SSRC + bytes[8..12].copy_from_slice(&self.ssrc.to_be_bytes()); + + bytes + } +} + +/// RTP Packet +#[derive(Debug, Clone)] +pub struct RtpPacket { + pub header: RtpHeader, + pub payload: Vec, +} + +impl RtpPacket { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(12 + self.payload.len()); + bytes.extend_from_slice(&self.header.to_bytes()); + bytes.extend_from_slice(&self.payload); + bytes + } +} + +/// H.264 RTP Packetizer +pub struct H264Packetizer { + mtu: usize, + payload_type: u8, + ssrc: u32, + sequence: u16, + timestamp: u32, + clock_rate: u32, // 90kHz for video + cached_sps: Option>, + cached_pps: Option>, + sps_pps_timestamp: u32, +} + +impl H264Packetizer { + pub fn new(mtu: usize) -> Self { + Self { + mtu, + payload_type: 96, // Dynamic payload type for H.264 + ssrc: rand::random(), + sequence: rand::random(), + timestamp: rand::random(), + clock_rate: 90000, // 90kHz + cached_sps: None, + cached_pps: None, + sps_pps_timestamp: 0, + } + } + + /// 封装单个 NAL unit 为 RTP packets + pub fn packetize(&mut self, nal: &NalUnit) -> Result> { + let mut packets = Vec::new(); + + use crate::rtp::annex_b_parser::NalType; + match nal.nal_type { + NalType::Sps => { + self.cached_sps = Some(nal.data.clone()); + self.sps_pps_timestamp = self.timestamp; + } + NalType::Pps => { + self.cached_pps = Some(nal.data.clone()); + self.sps_pps_timestamp = self.timestamp; + } + NalType::Idr => { + if self.sps_pps_timestamp != self.timestamp { + if let Some(sps) = &self.cached_sps { + let sps_nal = NalUnit { nal_type: NalType::Sps, data: sps.clone() }; + packets.push(self.create_single_nal_packet(&sps_nal, false)); + } + if let Some(pps) = &self.cached_pps { + let pps_nal = NalUnit { nal_type: NalType::Pps, data: pps.clone() }; + packets.push(self.create_single_nal_packet(&pps_nal, false)); + } + self.sps_pps_timestamp = self.timestamp; + } + } + _ => {} + } + + // RTP header 12 bytes, 需要为 payload 留空间 + let max_payload = self.mtu - 12; + let is_vcl = matches!(nal.nal_type, NalType::Slice | NalType::Idr); + + if nal.data.len() <= max_payload { + // Single NAL Unit Mode + packets.push(self.create_single_nal_packet(nal, is_vcl)); + } else { + // FU-A Fragmentation Mode + packets.extend(self.create_fua_packets(nal, is_vcl)); + } + + Ok(packets) + } + + /// 创建 Single NAL Unit 包 + fn create_single_nal_packet(&mut self, nal: &NalUnit, marker: bool) -> RtpPacket { + let header = RtpHeader { + version: 2, + padding: false, + extension: false, + marker, + payload_type: self.payload_type, + sequence: self.sequence, + timestamp: self.timestamp, + ssrc: self.ssrc, + }; + + self.sequence = self.sequence.wrapping_add(1); + + RtpPacket { + header, + payload: nal.data.clone(), + } + } + + /// 创建 FU-A 分片包 + fn create_fua_packets(&mut self, nal: &NalUnit, is_vcl: bool) -> Vec { + let nal_header = nal.data[0]; + let nal_type = nal_header & 0x1F; + let nal_nri = nal_header & 0xE0; + + // FU indicator: F(1)|NRI(2)|Type(5) = NRI|28 + let fu_indicator = nal_nri | 28; // 28 = FU-A + + // NAL payload (去掉 NAL header) + let payload_data = &nal.data[1..]; + + // 计算每个分片的大小(FU indicator + FU header + data) + let max_fragment_size = self.mtu - 12 - 2; // RTP header + FU headers + + let chunks: Vec<&[u8]> = payload_data + .chunks(max_fragment_size) + .collect(); + + chunks + .iter() + .enumerate() + .map(|(i, chunk)| { + let start = i == 0; + let end = i == chunks.len() - 1; + + // FU header: S(1)|E(1)|R(1)|Type(5) + let fu_header = ((start as u8) << 7) + | ((end as u8) << 6) + | nal_type; + + // Payload: FU indicator + FU header + fragment + let mut payload = Vec::with_capacity(2 + chunk.len()); + payload.push(fu_indicator); + payload.push(fu_header); + payload.extend_from_slice(chunk); + + let header = RtpHeader { + version: 2, + padding: false, + extension: false, + marker: end && is_vcl, // Marker on last fragment if VCL + payload_type: self.payload_type, + sequence: self.sequence, + timestamp: self.timestamp, + ssrc: self.ssrc, + }; + + self.sequence = self.sequence.wrapping_add(1); + + RtpPacket { header, payload } + }) + .collect() + } + + /// 更新时间戳(根据帧率) + /// 例如:30fps = 90000 / 30 = 3000 ticks per frame + pub fn update_timestamp(&mut self, duration_ticks: u32) { + self.timestamp = self.timestamp.wrapping_add(duration_ticks); + } + + /// 根据帧率计算时间戳增量 + pub fn timestamp_increment(&self, fps: u32) -> u32 { + self.clock_rate / fps + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rtp::annex_b_parser::{NalType, NalUnit}; + + #[test] + fn test_single_nal_packet() { + let mut packetizer = H264Packetizer::new(1500); + + let nal = NalUnit { + nal_type: NalType::Sps, + data: vec![0x67, 0x42, 0x00, 0x1e, 0x95, 0xa0, 0x14], + }; + + let packets = packetizer.packetize(&nal).unwrap(); + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].payload, nal.data); + assert!(!packets[0].header.marker); + } + + #[test] + fn test_fua_fragmentation() { + let mut packetizer = H264Packetizer::new(100); // Small MTU + + // Large NAL unit + let large_data = vec![0x65; 500]; // IDR slice header + data + let nal = NalUnit { + nal_type: NalType::Idr, + data: large_data, + }; + + let packets = packetizer.packetize(&nal).unwrap(); + + // Should be fragmented + assert!(packets.len() > 1); + + // Check FU-A indicators + for (i, packet) in packets.iter().enumerate() { + assert_eq!(packet.payload[0] & 0x1F, 28); // FU-A type + + if i == 0 { + // First fragment: S bit set + assert_eq!(packet.payload[1] & 0x80, 0x80); + } + if i == packets.len() - 1 { + // Last fragment: E bit set, marker bit set + assert_eq!(packet.payload[1] & 0x40, 0x40); + assert!(packet.header.marker); + } + } + } + + #[test] + fn test_timestamp_increment() { + let packetizer = H264Packetizer::new(1500); + assert_eq!(packetizer.timestamp_increment(30), 3000); // 30 fps + assert_eq!(packetizer.timestamp_increment(25), 3600); // 25 fps + } +} diff --git a/livesrc/src/rtp/mod.rs b/livesrc/src/rtp/mod.rs new file mode 100644 index 00000000..3719d070 --- /dev/null +++ b/livesrc/src/rtp/mod.rs @@ -0,0 +1,10 @@ +// RTP module for H.264 streaming +// Converts H.264 Annex B format to RTP packets + +pub mod annex_b_parser; +pub mod h264_packetizer; +pub mod sender; + +pub use annex_b_parser::{AnnexBParser, NalUnit, NalType}; +pub use h264_packetizer::{H264Packetizer, RtpHeader, RtpPacket}; +pub use sender::{create_rtp_sender, LiveionRtpSender, RtpSender, UdpRtpSender}; diff --git a/livesrc/src/rtp/sender.rs b/livesrc/src/rtp/sender.rs new file mode 100644 index 00000000..2a1318be --- /dev/null +++ b/livesrc/src/rtp/sender.rs @@ -0,0 +1,111 @@ +// RTP Sender interface +// Abstracts the transport layer for sending RTP packets + +use anyhow::Result; +use std::net::{SocketAddr, UdpSocket}; +use std::sync::Arc; + +use super::h264_packetizer::RtpPacket; + +/// RTP Sender trait +/// Different implementations for different transports +pub trait RtpSender: Send + Sync { + fn send(&self, packet: &RtpPacket) -> Result<()>; +} + +/// UDP RTP Sender (for testing and local transmission) +pub struct UdpRtpSender { + socket: UdpSocket, + dest_addr: SocketAddr, +} + +impl UdpRtpSender { + pub fn new(dest_addr: SocketAddr) -> Result { + let socket = UdpSocket::bind("0.0.0.0:0")?; + Ok(Self { socket, dest_addr }) + } +} + +impl RtpSender for UdpRtpSender { + fn send(&self, packet: &RtpPacket) -> Result<()> { + let bytes = packet.to_bytes(); + self.socket.send_to(&bytes, self.dest_addr)?; + Ok(()) + } +} + +/// Liveion RTP Sender (placeholder for future implementation) +/// Will be implemented once liveion provides the RTP receiver interface +pub struct LiveionRtpSender { + // TODO: Add fields based on liveion's interface + // Possible options: + // - gRPC channel + // - Unix domain socket + // - Shared memory + // - Custom protocol +} + +impl LiveionRtpSender { + pub fn new() -> Result { + // TODO: Initialize connection to liveion + todo!("Waiting for liveion RTP receiver interface specification") + } +} + +impl RtpSender for LiveionRtpSender { + fn send(&self, _packet: &RtpPacket) -> Result<()> { + // TODO: Implement based on liveion's interface + todo!("Waiting for liveion RTP receiver interface specification") + } +} + +/// Create RTP sender based on configuration +pub fn create_rtp_sender(mode: &str, address: Option<&str>) -> Result> { + match mode { + "udp" => { + let addr = address + .ok_or_else(|| anyhow::anyhow!("UDP mode requires address"))? + .parse()?; + Ok(Arc::new(UdpRtpSender::new(addr)?)) + } + "liveion" => { + Ok(Arc::new(LiveionRtpSender::new()?)) + } + _ => anyhow::bail!("Unknown RTP sender mode: {}", mode), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rtp::h264_packetizer::*; + + #[test] + fn test_create_udp_sender() { + let sender = create_rtp_sender("udp", Some("127.0.0.1:5004")); + assert!(sender.is_ok()); + } + + #[test] + fn test_udp_sender_send() { + let sender = UdpRtpSender::new("127.0.0.1:5004".parse().unwrap()).unwrap(); + + let packet = RtpPacket { + header: RtpHeader { + version: 2, + padding: false, + extension: false, + marker: true, + payload_type: 96, + sequence: 1234, + timestamp: 90000, + ssrc: 0x12345678, + }, + payload: vec![0x67, 0x42, 0x00, 0x1e], + }; + + // This will send to localhost - should not error + let result = sender.send(&packet); + assert!(result.is_ok()); + } +} diff --git a/livesrc/src/sources/libcamera.rs b/livesrc/src/sources/libcamera.rs new file mode 100644 index 00000000..6cc91bbb --- /dev/null +++ b/livesrc/src/sources/libcamera.rs @@ -0,0 +1,371 @@ +use anyhow::Result; +use std::os::unix::io::{AsRawFd, FromRawFd}; +use std::sync::{Arc, Mutex}; +use tokio::io::BufReader; +use tokio::process::{Child, Command}; +use tokio::task::JoinHandle; +use tracing::{debug, error, info, warn}; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::track::track_local::TrackLocalWriter; +use webrtc::util::Unmarshal; + +use crate::config::LibcameraConfig; +use crate::rtp::{AnnexBParser, H264Packetizer, NalType, RtpSender, UdpRtpSender}; + +use super::Source; + +/// Libcamera source handler for Raspberry Pi cameras +/// +/// Uses libcamera-bridge C++ executable for hardware-accelerated H.264 encoding, +/// then packages into RTP for transmission. +pub struct LibcameraSource { + config: LibcameraConfig, + track: Arc, + rtp_sender: Option>, + state: Arc>, +} + +struct SourceState { + child_process: Option, + child_stdin: Arc>>, + processor_handle: Option>, + running: bool, +} + +impl LibcameraSource { + pub fn new( + config: LibcameraConfig, + rtp_port: u16, + rtp_dest: Option, + track: Arc, + ) -> Self { + debug!("Creating LibcameraSource"); + + let dest_ip = rtp_dest.unwrap_or_else(|| "127.0.0.1".to_string()); + + // Create UDP RTP sender for testing + // TODO: Replace with LiveionRtpSender when liveion interface is ready + let rtp_sender = if rtp_port > 0 { + match UdpRtpSender::new(format!("{}:{}", dest_ip, rtp_port).parse().unwrap()) { + Ok(sender) => Some(Arc::new(sender) as Arc), + Err(e) => { + warn!("Failed to create UDP RTP sender: {}", e); + None + } + } + } else { + None + }; + + Self { + config, + track, + rtp_sender, + state: Arc::new(Mutex::new(SourceState { + child_process: None, + child_stdin: Arc::new(Mutex::new(None)), + processor_handle: None, + running: false, + })), + } + } + + /// Build libcamera-bridge command with configured parameters + fn build_libcamera_bridge_command(&self) -> Vec { + let cfg = &self.config; + + let mut args = vec![ + "--width".to_string(), + cfg.width.to_string(), + "--height".to_string(), + cfg.height.to_string(), + "--fps".to_string(), + cfg.fps.to_string(), + "--bitrate".to_string(), + cfg.bitrate.to_string(), + ]; + + // Add optional parameters + if cfg.camera_id > 0 { + args.push("--camera".to_string()); + args.push(cfg.camera_id.to_string()); + } + + if cfg.rotation > 0 { + args.push("--rotation".to_string()); + args.push(cfg.rotation.to_string()); + } + + if cfg.hflip { + args.push("--hflip".to_string()); + } + + if cfg.vflip { + args.push("--vflip".to_string()); + } + + args + } +} + +impl Source for LibcameraSource { + fn start(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + + if state.running { + warn!("Libcamera source already running"); + return Ok(()); + } + + let args = self.build_libcamera_bridge_command(); + info!("Starting libcamera-bridge with args: {:?}", args); + + // Clone necessary data for the async task + let track = self.track.clone(); + let rtp_sender = self.rtp_sender.clone(); + let fps = self.config.fps; + let stdin_handle = state.child_stdin.clone(); + + // Spawn blocking thread to run async code + let handle = std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + rt.block_on(async move { + // Start libcamera-bridge process with tokio + let mut child = match Command::new("libcamera-bridge") + .args(&args) + .stdin(std::process::Stdio::piped()) // Enable stdin for control + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) // Show errors! + .spawn() + { + Ok(child) => { + info!("libcamera-bridge started with PID: {:?}", child.id()); + child + } + Err(e) => { + error!("Failed to start libcamera-bridge: {}", e); + return; + } + }; + + // Take stdout and stdin from child process + if let Some(stdout) = child.stdout.take() { + let stdin = child.stdin.take(); + info!("H.264 stream processor started"); + if let Err(e) = Self::process_h264_stream(stdout, stdin, track, rtp_sender, fps).await { + error!("H.264 stream processor failed: {}", e); + } + } + + // Wait for child to finish + let _ = child.wait().await; + info!("libcamera-bridge process ended"); + }); + }); + + // Give the child process time to start + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Note: We can't easily save child_stdin here due to the async/thread boundary + // For now, keyframe requests won't work, but the basic functionality works + // TODO: Refactor to use tokio::spawn instead of std::thread::spawn + + state.processor_handle = None; // Can't store std::thread handle in JoinHandle<()> + state.running = true; + + info!("LibcameraSource started successfully with RTP output"); + Ok(()) + } + + fn stop(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + + if !state.running { + warn!("Libcamera source not running"); + return Ok(()); + } + + info!("Stopping libcamera source"); + + // Kill child process + if let Some(mut child) = state.child_process.take() { + debug!("Killing libcamera-bridge process"); + let _ = child.kill(); + let _ = child.wait(); + info!("libcamera-bridge process terminated"); + } + + // Abort processor task + if let Some(handle) = state.processor_handle.take() { + debug!("Aborting H.264 processor task"); + handle.abort(); + } + + state.running = false; + info!("LibcameraSource stopped"); + Ok(()) + } + + fn request_keyframe(&self) -> Result<()> { + use std::io::Write; + + let state = self.state.lock().unwrap(); + + if !state.running { + warn!("Cannot request keyframe: source not running"); + return Ok(()); + } + + // Get stdin handle + let mut stdin_guard = state.child_stdin.lock().unwrap(); + + if let Some(ref mut stdin) = *stdin_guard { + // Send 'k\n' command to libcamera-bridge + // Note: This is a blocking write, but should be fast + let raw_stdin = stdin.as_raw_fd(); + let mut file = unsafe { std::fs::File::from_raw_fd(raw_stdin) }; + match file.write_all(b"k\n") { + Ok(_) => { + match file.flush() { + Ok(_) => { + info!("✓ Keyframe requested"); + // Don't drop the file - it would close the fd + std::mem::forget(file); + Ok(()) + } + Err(e) => { + std::mem::forget(file); + warn!("Failed to flush keyframe request: {}", e); + Ok(()) // Don't fail the whole operation + } + } + } + Err(e) => { + std::mem::forget(file); + warn!("Failed to write keyframe request: {}", e); + Ok(()) // Don't fail the whole operation + } + } + } else { + warn!("No stdin handle available for keyframe request"); + Ok(()) + } + } +} + +impl LibcameraSource { + /// Process H.264 Annex B stream from libcamera-bridge + async fn process_h264_stream( + stdout: tokio::process::ChildStdout, + mut stdin: Option, + track: Arc, + rtp_sender: Option>, + fps: u32, + ) -> Result<()> { + // Directly use ChildStdout with BufReader - no unsafe conversion needed! + let reader = BufReader::new(stdout); + let mut parser = AnnexBParser::new(reader); + let mut packetizer = H264Packetizer::new(1200); // 1200 bytes MTU + + let timestamp_increment = packetizer.timestamp_increment(fps); + let mut frame_count = 0u64; + + info!("H.264 stream processor started"); + + // CRITICAL: Wait for libcamera-bridge to start up and begin outputting data + // Without this delay, the first read() returns 0 and parser treats it as EOF + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + info!("Starting to read H.264 stream"); + + loop { + // Read next NAL unit + let nal = match parser.read_next_nal().await { + Ok(Some(nal)) => nal, + Ok(None) => { + info!("End of H.264 stream"); + break; + } + Err(e) => { + error!("Failed to read NAL unit: {}", e); + break; + } + }; + + frame_count += 1; + if frame_count % 30 == 0 { + debug!("Processed {} NAL units, type: {:?}", frame_count, nal.nal_type); + } + + // Periodically request keyframes (e.g., every 60 frames = 2 seconds at 30fps) + if frame_count % 60 == 0 { + if let Some(ref mut stdin) = stdin { + use tokio::io::AsyncWriteExt; + if let Err(e) = stdin.write_all(b"k\n").await { + warn!("Failed to send keyframe request to libcamera-bridge stdin: {}", e); + } else if let Err(e) = stdin.flush().await { + warn!("Failed to flush keyframe request: {}", e); + } else { + info!("Requested periodic IDR keyframe from libcamera-bridge (frame {})", frame_count); + } + } + } + + // Package NAL into RTP packets + let rtp_packets = match packetizer.packetize(&nal) { + Ok(packets) => packets, + Err(e) => { + error!("Failed to packetize NAL: {}", e); + continue; + } + }; + + // Send RTP packets + for rtp_packet in &rtp_packets { + // Send via UDP (for testing) + if let Some(ref sender) = rtp_sender { + if let Err(e) = sender.send(rtp_packet) { + warn!("Failed to send RTP packet via UDP: {}", e); + } + } + + // Write to WebRTC track + let packet_bytes = rtp_packet.to_bytes(); + match webrtc::rtp::packet::Packet::unmarshal(&mut packet_bytes.as_slice()) { + Ok(webrtc_packet) => { + // Log every 30 frames to check if writes are happening + if frame_count % 30 == 0 { + debug!("Writing RTP packet to track (frame {})", frame_count); + } + + if let Err(e) = track.write_rtp(&webrtc_packet).await { + if frame_count % 30 == 0 { + warn!("Failed to write RTP to track (frame {}): {}", frame_count, e); + } + } else if frame_count % 30 == 0 { + debug!("Successfully wrote RTP packet to track (frame {})", frame_count); + } + } + Err(e) => { + warn!("Failed to unmarshal RTP packet: {}", e); + } + } + } + + // Only update timestamp after frame-level NAL units (IDR, Slice). + // SPS, PPS, SEI, AUD belong to the same access unit as the following + // IDR/Slice and MUST share the same RTP timestamp per RFC 6184. + match nal.nal_type { + NalType::Idr | NalType::Slice => { + packetizer.update_timestamp(timestamp_increment); + } + _ => { + // SPS, PPS, SEI, AUD, etc.: keep same timestamp + // — they are part of the same access unit as the next frame NAL + } + } + } + + info!("H.264 stream processor stopped after {} NAL units", frame_count); + Ok(()) + } +} diff --git a/livesrc/src/sources/mod.rs b/livesrc/src/sources/mod.rs new file mode 100644 index 00000000..58b80623 --- /dev/null +++ b/livesrc/src/sources/mod.rs @@ -0,0 +1,49 @@ +use anyhow::Result; + +/// Trait that all source handlers must implement. +pub trait Source { + /// Start the source (e.g., launch ffmpeg, open V4L2 device, connect to RTSP, etc.) + fn start(&self) -> Result<()>; + /// Stop the source and clean up resources. + fn stop(&self) -> Result<()>; + /// Request a keyframe (IDR) from the source. + /// This is critical for real-time streaming to ensure new subscribers get video immediately. + fn request_keyframe(&self) -> Result<()> { + // Default implementation does nothing (for sources that don't support it) + Ok(()) + } +} + +// ---------- Real implementations ---------- + +pub mod libcamera; +pub use libcamera::LibcameraSource; + +pub mod rpicam; +pub use rpicam::RpicamSource; + +// ---------- Placeholder implementations ---------- + +pub struct V4l2Source; +impl Source for V4l2Source { + fn start(&self) -> Result<()> { Ok(()) } + fn stop(&self) -> Result<()> { Ok(()) } +} + +pub struct WhipSource; +impl Source for WhipSource { + fn start(&self) -> Result<()> { Ok(()) } + fn stop(&self) -> Result<()> { Ok(()) } +} + +pub struct RtspSource; +impl Source for RtspSource { + fn start(&self) -> Result<()> { Ok(()) } + fn stop(&self) -> Result<()> { Ok(()) } +} + +pub struct PublisherSource; +impl Source for PublisherSource { + fn start(&self) -> Result<()> { Ok(()) } + fn stop(&self) -> Result<()> { Ok(()) } +} diff --git a/livesrc/src/sources/rpicam.rs b/livesrc/src/sources/rpicam.rs new file mode 100644 index 00000000..151ac62f --- /dev/null +++ b/livesrc/src/sources/rpicam.rs @@ -0,0 +1,309 @@ +use anyhow::Result; +use std::sync::{Arc, Mutex}; +use tokio::io::BufReader; +use tokio::process::Command; +use tracing::{debug, error, info, warn}; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::track::track_local::TrackLocalWriter; +use webrtc::util::Unmarshal; + +use crate::config::LibcameraConfig; +use crate::rtp::{AnnexBParser, H264Packetizer, NalType, RtpSender, UdpRtpSender}; + +use super::Source; + +/// RpicamSource: uses `rpicam-vid` (the official Raspberry Pi tool) to capture +/// and hardware-encode H.264 in one step. Much simpler than the custom +/// libcamera-bridge approach — no C++ code needed. +/// +/// Pipeline: +/// rpicam-vid --codec h264 --inline -t 0 -o - (stdout) +/// → AnnexBParser → H264Packetizer → RTP → UDP +pub struct RpicamSource { + config: LibcameraConfig, + track: Arc, + rtp_sender: Option>, + state: Arc>, +} + +struct RpicamState { + running: bool, +} + +impl RpicamSource { + pub fn new( + config: LibcameraConfig, + rtp_port: u16, + rtp_dest: Option, + track: Arc, + ) -> Self { + debug!("Creating RpicamSource (rpicam-vid backend)"); + + let dest_ip = rtp_dest.unwrap_or_else(|| "127.0.0.1".to_string()); + + let rtp_sender = if rtp_port > 0 { + match UdpRtpSender::new(format!("{}:{}", dest_ip, rtp_port).parse().unwrap()) { + Ok(sender) => Some(Arc::new(sender) as Arc), + Err(e) => { + warn!("Failed to create UDP RTP sender: {}", e); + None + } + } + } else { + None + }; + + Self { + config, + track, + rtp_sender, + state: Arc::new(Mutex::new(RpicamState { running: false })), + } + } + + /// Build the rpicam-vid command line arguments. + fn build_command_args(&self) -> Vec { + let cfg = &self.config; + + let mut args = vec![ + // Output H.264 Annex-B to stdout + "--codec".to_string(), + "h264".to_string(), + // Inline SPS/PPS headers (critical for stream joining) + "--inline".to_string(), + // Run forever + "-t".to_string(), + "0".to_string(), + // Output to stdout + "-o".to_string(), + "-".to_string(), + // Resolution + "--width".to_string(), + cfg.width.to_string(), + "--height".to_string(), + cfg.height.to_string(), + // Frame rate + "--framerate".to_string(), + cfg.fps.to_string(), + // Bitrate (rpicam-vid uses bits per second) + "--bitrate".to_string(), + cfg.bitrate.to_string(), + // H.264 profile: baseline for lowest latency + "--profile".to_string(), + "baseline".to_string(), + // H.264 level + "--level".to_string(), + "4".to_string(), + // Intra-refresh period (GOP size): one IDR every N frames + // 30 frames = 1 IDR per second at 30fps + "--intra".to_string(), + "30".to_string(), + // Flush output after every frame for lowest latency + "--flush".to_string(), + // Disable preview window (headless) + "-n".to_string(), + ]; + + // Camera selection + if cfg.camera_id > 0 { + args.push("--camera".to_string()); + args.push(cfg.camera_id.to_string()); + } + + // Rotation + if cfg.rotation > 0 { + args.push("--rotation".to_string()); + args.push(cfg.rotation.to_string()); + } + + // Flip + if cfg.hflip { + args.push("--hflip".to_string()); + } + if cfg.vflip { + args.push("--vflip".to_string()); + } + + args + } + + /// Process the H.264 Annex-B stream from rpicam-vid stdout. + /// This is the core loop: read NALs → packetize → send RTP. + async fn process_h264_stream( + stdout: tokio::process::ChildStdout, + track: Arc, + rtp_sender: Option>, + fps: u32, + ) -> Result<()> { + let reader = BufReader::new(stdout); + let mut parser = AnnexBParser::new(reader); + let mut packetizer = H264Packetizer::new(1200); // 1200 bytes MTU + + let timestamp_increment = packetizer.timestamp_increment(fps); + let mut frame_count = 0u64; + + info!("[rpicam] H.264 stream processor started"); + + // Give rpicam-vid a moment to initialize the camera hardware + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + info!("[rpicam] Starting to read H.264 stream from rpicam-vid stdout"); + + loop { + // Read next NAL unit from Annex-B stream + let nal = match parser.read_next_nal().await { + Ok(Some(nal)) => nal, + Ok(None) => { + info!("[rpicam] End of H.264 stream (rpicam-vid exited?)"); + break; + } + Err(e) => { + error!("[rpicam] Failed to read NAL unit: {}", e); + break; + } + }; + + frame_count += 1; + if frame_count % 30 == 0 { + debug!( + "[rpicam] Processed {} NAL units, latest type: {:?}", + frame_count, nal.nal_type + ); + } + + // Packetize NAL into RTP packets + let rtp_packets = match packetizer.packetize(&nal) { + Ok(packets) => packets, + Err(e) => { + error!("[rpicam] Failed to packetize NAL: {}", e); + continue; + } + }; + + // Send each RTP packet + for rtp_packet in &rtp_packets { + // Send via UDP to liveion + if let Some(ref sender) = rtp_sender { + if let Err(e) = sender.send(rtp_packet) { + warn!("[rpicam] Failed to send RTP packet via UDP: {}", e); + } + } + + // Also write to WebRTC track (for direct WHEP subscribers) + let packet_bytes = rtp_packet.to_bytes(); + match webrtc::rtp::packet::Packet::unmarshal(&mut packet_bytes.as_slice()) { + Ok(webrtc_packet) => { + if let Err(e) = track.write_rtp(&webrtc_packet).await { + if frame_count % 30 == 0 { + warn!( + "[rpicam] Failed to write RTP to track (frame {}): {}", + frame_count, e + ); + } + } + } + Err(e) => { + warn!("[rpicam] Failed to unmarshal RTP packet: {}", e); + } + } + } + + // Only advance timestamp after VCL NAL units (IDR, Slice). + // SPS/PPS/SEI belong to the same access unit → same timestamp. + match nal.nal_type { + NalType::Idr | NalType::Slice => { + packetizer.update_timestamp(timestamp_increment); + } + _ => { + // SPS, PPS, SEI, AUD: keep same timestamp + } + } + } + + info!( + "[rpicam] H.264 stream processor stopped after {} NAL units", + frame_count + ); + Ok(()) + } +} + +impl Source for RpicamSource { + fn start(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + + if state.running { + warn!("[rpicam] Source already running"); + return Ok(()); + } + + let args = self.build_command_args(); + info!("[rpicam] Starting rpicam-vid with args: {:?}", args); + + let track = self.track.clone(); + let rtp_sender = self.rtp_sender.clone(); + let fps = self.config.fps; + + // Spawn a dedicated thread with its own tokio runtime + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + rt.block_on(async move { + let mut child = match Command::new("rpicam-vid") + .args(&args) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .spawn() + { + Ok(child) => { + info!("[rpicam] rpicam-vid started with PID: {:?}", child.id()); + child + } + Err(e) => { + error!("[rpicam] Failed to start rpicam-vid: {}", e); + error!("[rpicam] Make sure rpicam-vid is installed (sudo apt install rpicam-apps)"); + return; + } + }; + + if let Some(stdout) = child.stdout.take() { + if let Err(e) = + Self::process_h264_stream(stdout, track, rtp_sender, fps).await + { + error!("[rpicam] H.264 stream processor failed: {}", e); + } + } + + let _ = child.wait().await; + info!("[rpicam] rpicam-vid process ended"); + }); + }); + + std::thread::sleep(std::time::Duration::from_millis(100)); + state.running = true; + + info!("[rpicam] RpicamSource started successfully"); + Ok(()) + } + + fn stop(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + + if !state.running { + warn!("[rpicam] Source not running"); + return Ok(()); + } + + info!("[rpicam] Stopping rpicam source"); + // rpicam-vid will be killed when the thread/runtime is dropped + state.running = false; + info!("[rpicam] RpicamSource stopped"); + Ok(()) + } + + fn request_keyframe(&self) -> Result<()> { + // rpicam-vid does not support on-demand keyframe requests via stdin. + // Keyframes are generated periodically based on the --intra parameter. + debug!("[rpicam] Keyframe request ignored (rpicam-vid uses fixed --intra interval)"); + Ok(()) + } +} diff --git a/livesrc/src/utils.rs b/livesrc/src/utils.rs new file mode 100644 index 00000000..52854863 --- /dev/null +++ b/livesrc/src/utils.rs @@ -0,0 +1,27 @@ +use std::fs; +use std::path::PathBuf; +use tracing::info; + +pub fn load(name: &str, config_path: Option) -> T +where + T: serde::de::DeserializeOwned + Default, +{ + let config_file = if let Some(path) = config_path { + PathBuf::from(path) + } else { + let mut path = PathBuf::from("conf"); + path.push(format!("{}.toml", name)); + path + }; + + if !config_file.exists() { + info!("Config file not found, using default configuration"); + return T::default(); + } + + let content = fs::read_to_string(&config_file) + .unwrap_or_else(|e| panic!("Failed to read config file {:?}: {}", config_file, e)); + + toml::from_str(&content) + .unwrap_or_else(|e| panic!("Failed to parse config file {:?}: {}", config_file, e)) +} diff --git a/livesrc/src/v4l2_capture.rs b/livesrc/src/v4l2_capture.rs new file mode 100644 index 00000000..f920c404 --- /dev/null +++ b/livesrc/src/v4l2_capture.rs @@ -0,0 +1,187 @@ +#[cfg(feature = "v4l2")] +use v4l::buffer::Type; +#[cfg(feature = "v4l2")] +use v4l::io::mmap::Stream; +#[cfg(feature = "v4l2")] +use v4l::io::traits::CaptureStream; +#[cfg(feature = "v4l2")] +use v4l::video::Capture; +#[cfg(feature = "v4l2")] +use v4l::{Device, FourCC}; + +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{debug, error, info, warn}; + +use crate::config::V4l2Config; + +#[cfg(feature = "v4l2")] +pub struct V4l2Capture<'a> { + device: Arc, + stream: Option>, + config: V4l2Config, +} + +#[cfg(feature = "v4l2")] +impl<'a> V4l2Capture<'a> { + pub fn new(device_path: &str, config: V4l2Config) -> anyhow::Result { + info!("Opening V4L2 device: {}", device_path); + + let device = Device::with_path(device_path)?; + + // Query device capabilities + let caps = device.query_caps()?; + info!( + "Device: {} (driver: {}, bus: {})", + caps.card, caps.driver, caps.bus + ); + + // Set format + let fourcc = match config.format.as_str() { + "H264" => FourCC::new(b"H264"), + "H265" | "HEVC" => FourCC::new(b"HEVC"), + _ => anyhow::bail!("Unsupported format: {}", config.format), + }; + + let mut fmt = device.format()?; + fmt.width = config.width; + fmt.height = config.height; + fmt.fourcc = fourcc; + device.set_format(&fmt)?; + + info!( + "Format set: {}x{} {} ({})", + fmt.width, fmt.height, config.format, fourcc + ); + + // Set frame rate + let mut params = device.params()?; + params.interval = v4l::Fraction::new(1, config.fps); + device.set_params(¶ms)?; + + info!("Frame rate set: {} fps", config.fps); + + Ok(Self { + device: Arc::new(device), + stream: None, + config, + }) + } + + pub fn start(&mut self) -> anyhow::Result<()> { + info!("Starting V4L2 capture stream"); + + // Create mmap stream with 4 buffers + let stream = Stream::with_buffers(&self.device, Type::VideoCapture, 4)?; + self.stream = Some(stream); + + info!("V4L2 capture stream started with 4 mmap buffers"); + Ok(()) + } + + pub async fn capture_loop( + &mut self, + frame_tx: mpsc::Sender>, + mut shutdown_rx: mpsc::Receiver<()>, + ) -> anyhow::Result<()> { + let stream = self.stream.as_mut() + .ok_or_else(|| anyhow::anyhow!("Stream not started"))?; + + info!("Starting V4L2 capture loop"); + let mut frame_count = 0u64; + + loop { + // Check for shutdown signal (non-blocking) + if shutdown_rx.try_recv().is_ok() { + info!("V4L2 capture loop shutdown requested"); + break; + } + + // Capture frame (blocking, but we'll use tokio::task::spawn_blocking) + let (buf, _meta) = match stream.next() { + Ok(frame) => frame, + Err(e) => { + error!("Failed to capture frame: {}", e); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + continue; + } + }; + + frame_count += 1; + + // Copy frame data + let frame_data = buf.to_vec(); + let frame_size = frame_data.len(); + + if frame_count % 30 == 0 { + debug!( + "Captured frame #{}: {} bytes", + frame_count, frame_size + ); + } + + // Send frame to RTP packetizer + if let Err(e) = frame_tx.send(frame_data).await { + error!("Failed to send frame to RTP packetizer: {}", e); + break; + } + + // Yield to allow other tasks to run + tokio::task::yield_now().await; + } + + info!("V4L2 capture loop ended. Total frames: {}", frame_count); + Ok(()) + } + + pub fn stop(&mut self) { + info!("Stopping V4L2 capture"); + self.stream = None; + } + + #[allow(dead_code)] + pub fn request_keyframe(&self) -> anyhow::Result<()> { + info!("Requesting IDR keyframe"); + + // Try to force keyframe using V4L2 control + // V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME + const V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME: u32 = 0x009909e6; + + use v4l::control::Control; + use v4l::control::Value; + + let ctrl = Control { + id: V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME, + value: Value::Integer(1), + }; + + match self.device.set_control(ctrl) { + Ok(_) => { + debug!("IDR keyframe requested successfully"); + Ok(()) + } + Err(e) => { + warn!("Failed to request keyframe: {}. Device may not support this control.", e); + Ok(()) // Don't fail, just warn + } + } + } +} + +#[cfg(feature = "v4l2")] +impl<'a> Drop for V4l2Capture<'a> { + fn drop(&mut self) { + self.stop(); + } +} + +// Stub implementation when v4l2 feature is not enabled +#[cfg(not(feature = "v4l2"))] +pub struct V4l2Capture; + +#[cfg(not(feature = "v4l2"))] +impl V4l2Capture { + pub fn new(_device_path: &str, _config: crate::config::V4l2Config) -> anyhow::Result { + anyhow::bail!("V4L2 support not enabled. Rebuild with --features v4l2") + } +} diff --git a/livesrc/src/whep_handler.rs b/livesrc/src/whep_handler.rs new file mode 100644 index 00000000..0026f221 --- /dev/null +++ b/livesrc/src/whep_handler.rs @@ -0,0 +1,332 @@ +use axum::{ + body::Bytes, + extract::{Path, State}, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + routing::post, + Router, +}; +use std::sync::Arc; +use std::time::Duration; +use tracing::{debug, error, info, warn}; +use webrtc::ice_transport::ice_server::RTCIceServer; +use webrtc::peer_connection::configuration::RTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; +use webrtc::track::track_local::TrackLocal; + +use crate::LiveSrcManager; + +pub fn create_router() -> Router { + Router::new() + // 保持向后兼容:/whep 使用默认 path + .route("/whep", post(handle_whep_default)) + // 新的多路径路由:/whep/{path_name} + .route("/whep/{path_name}", post(handle_whep_with_path)) +} + +/// 向后兼容的默认路由 - 使用旧的 LiveSrcManager +async fn handle_whep_default( + State(manager): State, + body: Bytes, +) -> Result { + let stream_id = &manager.stream_id; + info!(stream_id, "WHEP request received (legacy mode)"); + + let body_str = String::from_utf8_lossy(&body); + let offer = RTCSessionDescription::offer(body_str.to_string()).map_err(|e| { + error!("Failed to parse SDP offer: {}", e); + WhepError::InvalidSdp + })?; + + debug!("SDP offer parsed successfully"); + + // Get ICE servers from config + let ice_servers = { + let config = manager.config.read().unwrap(); + if config.webrtc.ice_servers.is_empty() { + vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_string()], + ..Default::default() + }] + } else { + vec![RTCIceServer { + urls: config.webrtc.ice_servers.clone(), + ..Default::default() + }] + } + }; + + let rtc_config = RTCConfiguration { + ice_servers, + ..Default::default() + }; + + // Create peer connection + let pc = manager + .webrtc_api + .new_peer_connection(rtc_config) + .await + .map_err(|e| { + error!("Failed to create peer connection: {}", e); + WhepError::InternalError + })?; + let pc = Arc::new(pc); + + info!("PeerConnection created"); + + // Add subscriber and get track (legacy mode - uses old add_subscriber) + let video_track = manager.add_subscriber().ok_or_else(|| { + error!("Failed to add subscriber"); + WhepError::InternalError + })?; + + pc.add_track(video_track as Arc) + .await + .map_err(|e| { + error!("Failed to add track: {}", e); + WhepError::InternalError + })?; + + debug!("Video track added to peer connection"); + + // Setup connection state handler + let pc_clone = pc.clone(); + let manager_clone = manager.clone(); + let stream_id_clone = stream_id.clone(); + pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { + let manager = manager_clone.clone(); + let pc = pc_clone.clone(); + let stream_id = stream_id_clone.clone(); + Box::pin(async move { + info!(stream_id, state = ?s, "PeerConnection state changed"); + match s { + RTCPeerConnectionState::Connected => { + info!(stream_id, "WebRTC connection established"); + } + RTCPeerConnectionState::Failed + | RTCPeerConnectionState::Disconnected + | RTCPeerConnectionState::Closed => { + warn!(stream_id, "Connection closed"); + manager.remove_subscriber(); + manager.remove_whep_session(); + let _ = pc.close().await; + } + _ => {} + } + }) + })); + + // Set remote description + pc.set_remote_description(offer).await.map_err(|e| { + error!("Failed to set remote description: {}", e); + WhepError::InternalError + })?; + + // Create answer + let answer = pc.create_answer(None).await.map_err(|e| { + error!("Failed to create answer: {}", e); + WhepError::InternalError + })?; + + // Set local description + let mut gather_complete = pc.gathering_complete_promise().await; + pc.set_local_description(answer).await.map_err(|e| { + error!("Failed to set local description: {}", e); + WhepError::InternalError + })?; + + // Wait for ICE gathering with timeout + let _ = tokio::time::timeout(Duration::from_secs(3), gather_complete.recv()).await; + + let local_desc = pc.local_description().await.ok_or_else(|| { + error!("No local description available"); + WhepError::InternalError + })?; + + // Store session + manager.set_whep_session(pc.clone()); + + info!(stream_id, "WHEP session created"); + + Response::builder() + .status(StatusCode::CREATED) + .header(header::CONTENT_TYPE, "application/sdp") + .header(header::LOCATION, format!("/session/{}", stream_id)) + .body(local_desc.sdp.into()) + .map_err(|e| { + error!("Failed to build response: {}", e); + WhepError::InternalError + }) +} + +/// 新的多路径 WHEP handler - 使用 PathManager +async fn handle_whep_with_path( + State(manager): State, + Path(path_name): Path, + body: Bytes, +) -> Result { + info!(path = %path_name, "WHEP request received for path"); + + let body_str = String::from_utf8_lossy(&body); + let offer = RTCSessionDescription::offer(body_str.to_string()).map_err(|e| { + error!(path = %path_name, "Failed to parse SDP offer: {}", e); + WhepError::InvalidSdp + })?; + + debug!("SDP offer parsed successfully"); + + // Get ICE servers from config + let ice_servers = { + let config = manager.config.read().unwrap(); + if config.webrtc.ice_servers.is_empty() { + vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_string()], + ..Default::default() + }] + } else { + vec![RTCIceServer { + urls: config.webrtc.ice_servers.clone(), + ..Default::default() + }] + } + }; + + let rtc_config = RTCConfiguration { + ice_servers, + ..Default::default() + }; + + // Create peer connection + let pc = manager + .webrtc_api + .new_peer_connection(rtc_config) + .await + .map_err(|e| { + error!("Failed to create peer connection: {}", e); + WhepError::InternalError + })?; + let pc = Arc::new(pc); + + info!("PeerConnection created"); + + // Add subscriber via PathManager + let video_track = manager + .path_manager + .add_subscriber(&path_name) + .map_err(|e| { + error!(path = %path_name, "Failed to add subscriber: {}", e); + // 检查错误类型 + let err_msg = e.to_string(); + if err_msg.contains("not found") { + WhepError::PathNotFound + } else if err_msg.contains("exceeded max_readers") { + WhepError::MaxReadersExceeded + } else { + WhepError::InternalError + } + })? + .ok_or_else(|| { + error!(path = %path_name, "PathManager returned None for track"); + WhepError::InternalError + })?; + + pc.add_track(video_track as Arc) + .await + .map_err(|e| { + error!("Failed to add track: {}", e); + // 回滚订阅者计数 + let _ = manager.path_manager.remove_subscriber(&path_name); + WhepError::InternalError + })?; + + debug!("Video track added to peer connection"); + + // Setup connection state handler + let pc_clone = pc.clone(); + let path_manager = manager.path_manager.clone(); + let path_name_clone = path_name.clone(); + pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { + let path_manager = path_manager.clone(); + let pc = pc_clone.clone(); + let path_name = path_name_clone.clone(); + Box::pin(async move { + info!(path = %path_name, state = ?s, "PeerConnection state changed"); + match s { + RTCPeerConnectionState::Connected => { + info!(path = %path_name, "WebRTC connection established"); + } + RTCPeerConnectionState::Failed + | RTCPeerConnectionState::Disconnected + | RTCPeerConnectionState::Closed => { + warn!(path = %path_name, "Connection closed"); + // 通过 PathManager 移除订阅者 + if let Err(e) = path_manager.remove_subscriber(&path_name) { + error!(path = %path_name, "Failed to remove subscriber: {}", e); + } + let _ = pc.close().await; + } + _ => {} + } + }) + })); + + // Set remote description + pc.set_remote_description(offer).await.map_err(|e| { + error!("Failed to set remote description: {}", e); + WhepError::InternalError + })?; + + // Create answer + let answer = pc.create_answer(None).await.map_err(|e| { + error!("Failed to create answer: {}", e); + WhepError::InternalError + })?; + + // Set local description + let mut gather_complete = pc.gathering_complete_promise().await; + pc.set_local_description(answer).await.map_err(|e| { + error!("Failed to set local description: {}", e); + WhepError::InternalError + })?; + + // Wait for ICE gathering with timeout + let _ = tokio::time::timeout(Duration::from_secs(3), gather_complete.recv()).await; + + let local_desc = pc.local_description().await.ok_or_else(|| { + error!("No local description available"); + WhepError::InternalError + })?; + + info!(path = %path_name, "WHEP session created"); + + Response::builder() + .status(StatusCode::CREATED) + .header(header::CONTENT_TYPE, "application/sdp") + .header(header::LOCATION, format!("/session/{}", path_name)) + .body(local_desc.sdp.into()) + .map_err(|e| { + error!("Failed to build response: {}", e); + WhepError::InternalError + }) +} + +#[derive(Debug)] +pub enum WhepError { + InvalidSdp, + InternalError, + PathNotFound, + MaxReadersExceeded, +} + +impl IntoResponse for WhepError { + fn into_response(self) -> Response { + let (status, message) = match self { + WhepError::InvalidSdp => (StatusCode::BAD_REQUEST, "Invalid SDP"), + WhepError::InternalError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"), + WhepError::PathNotFound => (StatusCode::NOT_FOUND, "Path not found"), + WhepError::MaxReadersExceeded => (StatusCode::SERVICE_UNAVAILABLE, "Maximum number of readers exceeded"), + }; + (status, message).into_response() + } +} diff --git a/src/livesrc.rs b/src/livesrc.rs new file mode 100644 index 00000000..729f8b14 --- /dev/null +++ b/src/livesrc.rs @@ -0,0 +1,60 @@ +use clap::{Parser, Subcommand}; +use std::sync::{Arc, RwLock}; +use tracing::{debug, info, warn}; + +mod log; +mod utils; + +use livesrc::config::Config; + +#[derive(Parser)] +#[command(name = "livesrc", version)] +struct Args { + #[command(subcommand)] + command: Option, + + #[arg(short, long)] + config: Option, +} + +#[derive(Subcommand)] +enum Commands { + Serve, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + match args.command { + Some(Commands::Serve) | None => {} + } + + let mut cfg: Config = livesrc::utils::load("livesrc", args.config); + cfg.validate().unwrap(); + + log::set(format!( + "livesrc={},tower_http=info,webrtc=error", + cfg.log.level + )); + + warn!("set log level: {}", cfg.log.level); + debug!("load config: {:?}", cfg); + + let listener = match tokio::net::TcpListener::bind(&cfg.http.listen).await { + Ok(l) => l, + Err(e) => { + tracing::error!("bind to {} failed: {}", &cfg.http.listen, e); + return; + } + }; + info!("livesrc server listening on: {}", &cfg.http.listen); + + let config_arc = Arc::new(RwLock::new(cfg)); + + if let Err(e) = livesrc::serve(config_arc, listener, utils::shutdown_signal()).await { + tracing::error!("server error: {}", e); + } + + info!("livesrc server shutdown"); +} diff --git a/src/main.rs b/src/main.rs index 3e78f7d7..ab780014 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ struct Args { #[arg(short = 'v', action = ArgAction::Count)] verbose: u8, /// Set config file path - #[arg(short, long, default_value_t = format!("{}.toml", "live777"))] + #[arg(short, long, default_value_t = format!("{}.toml", if option_env!("CXXFLAGS").unwrap_or("").contains("PLATFORM_RDK") { "livesrc-rdk" } else { "live777" }))] config: String, } diff --git a/src/utils.rs b/src/utils.rs index aaf589fe..37a46b8e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,3 +4,36 @@ pub async fn shutdown_signal() { let str = signal::wait_for_stop_signal().await; debug!("Received signal: {}", str); } + +#[allow(dead_code)] +pub fn load(name: String, path_opt: Option) -> T +where + T: serde::de::DeserializeOwned + std::default::Default, +{ + use std::fs::read_to_string; + use std::process::exit; + + let content = if let Some(p) = path_opt { + // User explicitly specified a path, it MUST exist + match read_to_string(&p) { + Ok(s) => s, + Err(e) => { + eprintln!("CRITICAL: Failed to read config file at '{}': {}", p, e); + exit(1); + } + } + } else { + // Default lookup logic + read_to_string(format!("{name}.toml")) + .or_else(|_| read_to_string(format!("/etc/live777/{name}.toml"))) + .unwrap_or_else(|_| "".to_string()) + }; + + match toml::from_str(&content) { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("CRITICAL: Config parsing error: {}", err); + exit(1); + } + } +}