lamco-video-0.1.8/.cargo_vcs_info.json0000644000000001601046102023000132600ustar { "git": { "sha1": "87ac0d902df087af3aa706e6f09dca616e53d20f" }, "path_in_vcs": "crates/lamco-video" }lamco-video-0.1.8/CHANGELOG.md000064400000000000000000000057011046102023000136450ustar 00000000000000# Changelog All notable changes to lamco-video will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.1.6] - 2026-03-26 ### Changed - Updated for lamco-pipewire 0.4.0 FrameBuffer API - Converter uses `frame.data()` accessor instead of direct field access - Requires lamco-pipewire >= 0.4.0 ## [0.1.5] - 2026-03-15 ### Changed - Bump to Rust edition 2024, minimum supported Rust version 1.85 - Minor clippy fixes for edition 2024 compatibility ## [0.1.4] - 2026-03-04 ### Changed - Updated dependency: lamco-pipewire 0.2.0 → 0.3.0 ## [0.1.3] - 2026-02-26 ### Changed - Updated dependency: lamco-pipewire 0.1.3 → 0.2.0 ## [0.1.2] - 2025-12-23 ### Changed - Updated dependency: lamco-pipewire 0.1.2 → 0.1.3 ## [0.1.1] - 2025-12-17 ### Fixed - Added `#![cfg_attr(docsrs, feature(doc_cfg))]` for proper docs.rs conditional documentation - Converted to workspace package inheritance (edition, rust-version, license, homepage, repository, authors) ### Added - Added CHANGELOG.md ### Note - docs.rs builds will fail for this crate because it depends on lamco-pipewire which requires `libpipewire-0.3` system library not available in the docs.rs build environment. This is expected and unavoidable. ## [0.1.0] - 2025-12-15 ### Added - Initial release on crates.io - **`FrameProcessor`** - Video frame processing pipeline - Frame rate limiting with configurable target FPS - Age-based frame dropping - Queue depth management - Adaptive quality support - **`ProcessorConfig`** - Processor configuration - Target FPS, queue depth, damage threshold - Metrics collection toggle - **`BitmapConverter`** - RDP bitmap conversion - PipeWire frame to RDP bitmap format - Multiple output formats (BgrX32, Bgr24, Rgb16, Rgb15) - Buffer pooling for memory efficiency - **`BitmapUpdate`** - RDP-ready output - Rectangle-based updates - Damage region optimization - **`FrameDispatcher`** - Multi-stream coordination - Priority-based dispatch - Backpressure handling with high/low water marks - Load balancing across streams - **`DispatcherConfig`** - Dispatcher configuration - Channel size, priority dispatch, max frame age - Backpressure thresholds - Typed error handling with `ConversionError`, `ProcessingError`, `DispatchError` - Statistics collection for monitoring ### Platform Support - Linux only (requires lamco-pipewire) [Unreleased]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-video-v0.1.3...HEAD [0.1.3]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-video-v0.1.2...lamco-video-v0.1.3 [0.1.2]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-video-v0.1.1...lamco-video-v0.1.2 [0.1.1]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-video-v0.1.0...lamco-video-v0.1.1 [0.1.0]: https://github.com/lamco-admin/lamco-wayland/releases/tag/lamco-video-v0.1.0 lamco-video-0.1.8/Cargo.lock0000644000000533261046102023000112470ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "annotate-snippets" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", "unicode-width", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "annotate-snippets", "bitflags", "cexpr", "clang-sys", "itertools", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cc" version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom 7.1.3", ] [[package]] name = "cfg-expr" version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[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 = "convert_case" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" dependencies = [ "unicode-segmentation", ] [[package]] name = "cookie-factory" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "lamco-pipewire" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87911d1a49cc05932da09594374235bfe4fc4e497ab870faa9e642e509d013e1" dependencies = [ "futures", "libc", "libspa", "libspa-sys", "nix", "parking_lot", "pipewire", "thiserror 1.0.69", "tokio", "tracing", ] [[package]] name = "lamco-video" version = "0.1.8" dependencies = [ "lamco-pipewire", "parking_lot", "thiserror 1.0.69", "tokio", "tracing", "tracing-subscriber", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[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 = "libspa" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" dependencies = [ "bitflags", "cc", "convert_case", "cookie-factory", "libc", "libspa-sys", "nix", "nom 8.0.0", "system-deps", ] [[package]] name = "libspa-sys" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" dependencies = [ "bindgen", "cc", "system-deps", ] [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nom" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", ] [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ "windows-sys", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipewire" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" dependencies = [ "anyhow", "bitflags", "libc", "libspa", "libspa-sys", "nix", "once_cell", "pipewire-sys", "thiserror 2.0.17", ] [[package]] name = "pipewire-sys" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" dependencies = [ "bindgen", "libspa-sys", "system-deps", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_spanned" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "system-deps" version = "7.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr", "heck", "pkg-config", "toml", "version-compare", ] [[package]] name = "target-lexicon" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "pin-project-lite", "tokio-macros", ] [[package]] name = "tokio-macros" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tracing" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-compare" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" lamco-video-0.1.8/Cargo.toml0000644000000050711046102023000112640ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.85" name = "lamco-video" version = "0.1.8" authors = ["Greg Lamberson "] build = false exclude = [ "/.github/", "/tests/", "*.orig", "*.original", "*.bak", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Video frame processing and bitmap conversion for Wayland screen capture, by Lamco Development" homepage = "https://lamco.ai" documentation = "https://docs.rs/lamco-video" readme = "README.md" keywords = [ "video", "bitmap", "wayland", "screen-capture", "frame", ] categories = [ "multimedia::video", "graphics", "asynchronous", ] license = "MIT OR Apache-2.0" repository = "https://github.com/lamco-admin/lamco-wayland" resolver = "2" [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] rustdoc-args = [ "--cfg", "docsrs", ] [badges.maintenance] status = "actively-developed" [features] damage = [] default = [] full = ["damage"] [lib] name = "lamco_video" path = "src/lib.rs" [[example]] name = "basic" path = "examples/basic.rs" [dependencies.lamco-pipewire] version = "0.4.0" [dependencies.parking_lot] version = "0.12" [dependencies.thiserror] version = "1.0" [dependencies.tokio] version = "1" features = [ "sync", "rt", "time", ] [dependencies.tracing] version = "0.1" [dev-dependencies.tokio] version = "1" features = [ "sync", "rt", "time", "macros", "rt-multi-thread", ] [dev-dependencies.tracing-subscriber] version = "0.3" [lints.clippy] as_conversions = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" large_futures = "warn" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" needless_range_loop = "allow" or_fun_call = "warn" panic = "warn" rc_buffer = "warn" similar_names = "warn" unwrap_used = "warn" wildcard_imports = "warn" [lints.rust] elided_lifetimes_in_paths = "warn" single_use_lifetimes = "warn" unreachable_pub = "warn" unsafe_code = "warn" lamco-video-0.1.8/Cargo.toml.orig000064400000000000000000000106001046102023000147150ustar 00000000000000[package] # ============================================================================ # PACKAGE IDENTITY # ============================================================================ name = "lamco-video" version = "0.1.8" # ============================================================================ # PACKAGE CONFIGURATION (inherited from workspace) # ============================================================================ edition.workspace = true rust-version.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true authors.workspace = true # ============================================================================ # CRATE-SPECIFIC METADATA # ============================================================================ description = "Video frame processing and bitmap conversion for Wayland screen capture, by Lamco Development" documentation = "https://docs.rs/lamco-video" readme = "README.md" # Keywords for crates.io search (max 5, max 20 chars each) keywords = ["video", "bitmap", "wayland", "screen-capture", "frame"] # Categories for crates.io browsing (max 5) # See: https://crates.io/category_slugs categories = [ "multimedia::video", # Video processing "graphics", # Bitmap/image operations "asynchronous", # Async/await API design ] # ============================================================================ # PUBLISHING CONFIGURATION # ============================================================================ # Files to exclude from the published crate (reduces download size) exclude = [ "/.github/", "/tests/", "*.orig", "*.original", "*.bak", ] # ============================================================================ # BADGES (maintenance status) # ============================================================================ [badges] maintenance = { status = "actively-developed" } # ============================================================================ # DOCS.RS CONFIGURATION # ============================================================================ [package.metadata.docs.rs] # Build docs with all features enabled for comprehensive documentation all-features = true # docs.rs is Linux-only, which matches our target targets = ["x86_64-unknown-linux-gnu"] # Allow building even if lamco-pipewire fails (it needs PipeWire system libs) # The documentation will still be useful as API reference rustdoc-args = ["--cfg", "docsrs"] # ============================================================================ # FEATURES # ============================================================================ [features] default = [] # Full damage region tracking with advanced comparison damage = [] # Enable all optional features full = ["damage"] # ============================================================================ # LINTS (manual - requires numeric cast overrides for video processing) # Video processing requires extensive numeric conversions for stride # calculations, pixel format conversion, and damage region handling. # ============================================================================ [lints.rust] unsafe_code = "warn" elided_lifetimes_in_paths = "warn" single_use_lifetimes = "warn" unreachable_pub = "warn" [lints.clippy] # Numeric casts - ALLOWED for video processing code as_conversions = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" # Correctness unwrap_used = "warn" panic = "warn" # Style and readability similar_names = "warn" wildcard_imports = "warn" # Performance large_futures = "warn" rc_buffer = "warn" or_fun_call = "warn" # Relaxed lints missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" needless_range_loop = "allow" # ============================================================================ # DEPENDENCIES # ============================================================================ [dependencies] # PipeWire frame types (VideoFrame, PixelFormat, DamageRegion) lamco-pipewire = { path = "../lamco-pipewire", version = "0.4.0" } # Async runtime tokio = { version = "1", features = ["sync", "rt", "time"] } # Logging tracing = "0.1" # Error handling thiserror = "1.0" # Concurrency parking_lot = "0.12" [dev-dependencies] tokio = { version = "1", features = ["sync", "rt", "time", "macros", "rt-multi-thread"] } tracing-subscriber = "0.3" lamco-video-0.1.8/LICENSE-APACHE000064400000000000000000000240531046102023000137610ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to the Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2025 Greg Lamberson Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. lamco-video-0.1.8/LICENSE-MIT000064400000000000000000000020571046102023000134710ustar 00000000000000MIT License Copyright (c) 2025 Greg Lamberson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lamco-video-0.1.8/PUBLICATION_REVIEW.md000064400000000000000000000126051046102023000152110ustar 00000000000000# lamco-video Publication Review ## Summary **Package:** lamco-video v0.1.0 **Status:** Ready for publication review **Date:** 2025-12-15 ## Package Contents | Metric | Value | |--------|-------| | Files packaged | 11 | | Uncompressed size | 107.1 KB | | Compressed size | 28.0 KB | | Unit tests | 22 | | Doc tests | 2 | | Warnings | 3 (all expected `unsafe impl Send` for marker traits) | ## Files Included ``` Cargo.toml LICENSE-APACHE LICENSE-MIT README.md src/lib.rs src/converter.rs src/dispatcher.rs src/processor.rs examples/basic.rs ``` ## Features | Feature | Default | Description | |---------|---------|-------------| | `damage` | No | Full damage region tracking | | `full` | No | All features enabled | ## Dependencies | Crate | Version | Purpose | |-------|---------|---------| | lamco-pipewire | 0.1 | VideoFrame, PixelFormat types | | tokio | 1 | Async runtime (sync, rt, time) | | tracing | 0.1 | Logging | | thiserror | 1.0 | Error handling | | parking_lot | 0.12 | Synchronization primitives | ## API Surface ### Primary Types - **`BitmapConverter`** - Converts VideoFrame to RDP bitmap format - **`FrameProcessor`** - Processes frames with rate limiting and queueing - **`FrameDispatcher`** - Multi-stream priority-based frame routing ### Configuration - **`ProcessorConfig`** - Frame processor settings (FPS, queue depth, etc.) - **`DispatcherConfig`** - Dispatcher settings (backpressure, priorities) ### Output Types - **`BitmapUpdate`** - Contains list of `BitmapData` rectangles - **`BitmapData`** - Single region with pixel data ready for RDP - **`Rectangle`** - Region coordinates ### Pixel Formats - **`RdpPixelFormat`** - BgrX32, Bgr24, Rgb16, Rgb15 ### Statistics - **`ConversionStats`** - Frames converted, bytes processed, timing - **`ProcessingStats`** - Frame counts, drop rates, queue depths - **`DispatcherStats`** - Dispatch rates, backpressure status ### Error Types - **`ConversionError`** - Format conversion failures - **`ProcessingError`** - Frame processing failures - **`DispatchError`** - Dispatch/channel errors ## Test Results ``` running 22 tests test converter::tests::test_bitmap_converter_creation ... ok test converter::tests::test_buffer_pool ... ok test converter::tests::test_conversion_stats ... ok test converter::tests::test_damage_tracker ... ok test converter::tests::test_rdp_pixel_format ... ok test converter::tests::test_rectangle_operations ... ok test converter::tests::test_stride_calculation ... ok test dispatcher::tests::test_dispatch_frame ... ok test dispatcher::tests::test_dispatcher_config ... ok test dispatcher::tests::test_dispatcher_creation ... ok test dispatcher::tests::test_dispatcher_lifecycle ... ok test dispatcher::tests::test_dispatcher_stats ... ok test dispatcher::tests::test_stream_priority ... ok test dispatcher::tests::test_stream_registration ... ok test processor::tests::test_processing_stats ... ok test processor::tests::test_processor_config ... ok test processor::tests::test_processor_creation ... ok test processor::tests::test_processor_lifecycle ... ok test processor::tests::test_queued_frame ... ok test tests::test_calculate_rdp_stride ... ok test tests::test_recommended_queue_size ... ok test tests::test_version ... ok test result: ok. 22 passed; 0 failed; 0 ignored ``` ## Known Warnings The crate has 3 expected warnings: ``` warning: implementation of an `unsafe` trait --> src/converter.rs:642:1 | 642 | unsafe impl Send for BitmapConverter {} ``` These are **intentional** unsafe marker trait implementations. The types contain only Send-safe fields but cannot auto-derive Send due to internal structure. Each is documented with a SAFETY comment explaining why the implementation is correct. ## Lint Configuration The crate uses relaxed cast-related lints appropriate for video processing: ```toml [lints.clippy] # Relaxed for video processing code (heavy numeric conversions) as_conversions = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" # Still enforced unwrap_used = "warn" panic = "warn" ``` This matches the nature of low-level video processing code that requires extensive numeric conversions for stride calculations, pixel format conversion, and damage region handling. ## Issues to Resolve Before Publishing ### None - Ready for Publication All issues have been resolved: 1. ✅ Imports updated from `crate::pipewire::*` to `lamco_pipewire::*` 2. ✅ All tests passing 3. ✅ Clippy clean (only expected warnings) 4. ✅ Documentation complete 5. ✅ README created 6. ✅ License files added 7. ✅ Examples created 8. ✅ Dry run successful ## Feature Discussion The current feature set is minimal: - **`damage`** - Enables full damage region tracking (currently all code is unconditionally compiled) - **`full`** - Alias for all features ### Potential Future Features (not implemented) These could be added in future versions: 1. **`compression`** - RDP bitmap compression (RLE, etc.) 2. **`vaapi`** - VAAPI hardware encoding integration 3. **`openh264`** - Software H.264 encoding 4. **`statistics`** - Extended performance metrics ## Publication Command When ready to publish: ```bash cd /home/greg/lamco-admin/projects/lamco-rust-crates/staging/lamco-video cargo publish ``` ## Related Crates | Crate | Version | Status | |-------|---------|--------| | lamco-portal | 0.1.0 | Published | | lamco-pipewire | 0.1.0 | Published | | lamco-video | 0.1.0 | Ready for review | lamco-video-0.1.8/README.md000064400000000000000000000125341046102023000133150ustar 00000000000000# lamco-video Video frame processing and RDP bitmap conversion for Wayland screen capture. [![Crates.io](https://img.shields.io/crates/v/lamco-video.svg)](https://crates.io/crates/lamco-video) [![Documentation](https://docs.rs/lamco-video/badge.svg)](https://docs.rs/lamco-video) [![License](https://img.shields.io/crates/l/lamco-video.svg)](LICENSE-MIT) ## Features - **Frame Processing Pipeline**: Configurable video frame processing with rate limiting - **RDP Bitmap Conversion**: Convert PipeWire frames to RDP-ready bitmap format - **Damage Region Tracking**: Optimize updates by only sending changed regions - **Buffer Pooling**: Efficient memory management with reusable buffers - **Priority-Based Dispatch**: Multi-stream coordination with backpressure handling - **SIMD Optimization**: Automatic use of SIMD instructions where available ## Quick Start ```rust,ignore use lamco_video::{FrameProcessor, ProcessorConfig, BitmapConverter}; use lamco_pipewire::VideoFrame; use tokio::sync::mpsc; // Create frame processor let config = ProcessorConfig::default(); let processor = std::sync::Arc::new(FrameProcessor::new(config, 1920, 1080)); // Create channels let (input_tx, input_rx) = mpsc::channel(30); let (output_tx, mut output_rx) = mpsc::channel(30); // Start processor let processor_clone = processor.clone(); tokio::spawn(async move { processor_clone.start(input_rx, output_tx).await }); // Send frames from lamco-pipewire, receive bitmap updates while let Some(bitmap_update) = output_rx.recv().await { for rect in &bitmap_update.rectangles { println!("Update region: {:?}", rect.rectangle); } } ``` ## Configuration ### Processor Configuration ```rust use lamco_video::ProcessorConfig; let config = ProcessorConfig { target_fps: 60, // Target frame rate max_queue_depth: 30, // Max frames in queue before dropping adaptive_quality: true, // Enable adaptive quality damage_threshold: 0.05, // Minimum damage area to process (5%) drop_on_full_queue: true, // Drop frames when queue is full enable_metrics: true, // Enable statistics collection }; ``` ### Dispatcher Configuration ```rust use lamco_video::DispatcherConfig; let config = DispatcherConfig { channel_size: 30, // Buffer size per stream priority_dispatch: true, // Enable priority-based dispatch max_frame_age_ms: 150, // Drop frames older than 150ms enable_backpressure: true, // Enable backpressure handling high_water_mark: 0.8, // Trigger backpressure at 80% low_water_mark: 0.5, // Release backpressure at 50% load_balancing: true, // Enable load balancing }; ``` ## Architecture The processing pipeline: ```text ┌────────────────────┐ │ lamco-pipewire │ │ (VideoFrame) │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ FrameDispatcher │ ◄── Multi-stream routing │ (priority queue) │ Backpressure handling └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ FrameProcessor │ ◄── Frame rate limiting │ (rate control) │ Age-based dropping └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ BitmapConverter │ ◄── Pixel format conversion │ (format conv) │ Damage region tracking └─────────┬──────────┘ Buffer pooling │ ▼ ┌────────────────────┐ │ BitmapUpdate │ ◄── RDP-ready rectangles │ (RDP output) │ └────────────────────┘ ``` ## RDP Pixel Formats The converter supports these RDP-compatible output formats: | Format | BPP | Description | |--------|-----|-------------| | BgrX32 | 4 | 32-bit BGRX (most common) | | Bgr24 | 3 | 24-bit BGR | | Rgb16 | 2 | 16-bit RGB 5:6:5 | | Rgb15 | 2 | 15-bit RGB 5:5:5 | ## Feature Flags | Feature | Default | Description | |---------|---------|-------------| | `damage` | No | Full damage region tracking | | `full` | No | All features enabled | ```toml [dependencies] lamco-video = { version = "0.1", features = ["full"] } ``` ## Performance Typical performance on modern hardware: - **Conversion latency**: < 1ms per frame (1080p) - **Memory usage**: < 50MB (with buffer pooling) - **Throughput**: > 200 MB/s (with SIMD) - **Frame rates**: Tested up to 144Hz ## About Developed by [Lamco Development](https://lamco.ai). Part of the lamco-wayland ecosystem for building Wayland-native applications in Rust. ## Requirements - **Linux** with a Wayland compositor - **Rust 1.85+** ## Related Crates - [`lamco-portal`](https://crates.io/crates/lamco-portal) - XDG Desktop Portal integration - [`lamco-pipewire`](https://crates.io/crates/lamco-pipewire) - PipeWire screen capture ## License Licensed under either of: - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. lamco-video-0.1.8/examples/basic.rs000064400000000000000000000063461046102023000153070ustar 00000000000000//! Basic lamco-video usage example //! //! This example demonstrates the basic components of lamco-video: //! - ProcessorConfig for configuring frame processing //! - BitmapConverter for pixel format conversion //! - Rectangle and damage region handling //! //! Note: This example doesn't process actual frames - it just //! demonstrates the API. For actual frame processing, you need //! lamco-pipewire to capture frames from PipeWire. use lamco_video::{BitmapConverter, DispatcherConfig, ProcessorConfig, RdpPixelFormat, Rectangle}; fn main() { println!("lamco-video v{}", lamco_video::VERSION); println!(); // Demonstrate processor configuration let processor_config = ProcessorConfig { target_fps: 60, max_queue_depth: 30, adaptive_quality: true, damage_threshold: 0.05, drop_on_full_queue: true, enable_metrics: true, }; println!("Processor Config:"); println!(" Target FPS: {}", processor_config.target_fps); println!(" Max queue depth: {}", processor_config.max_queue_depth); println!(" Adaptive quality: {}", processor_config.adaptive_quality); println!(); // Demonstrate dispatcher configuration let dispatcher_config = DispatcherConfig::default(); println!("Dispatcher Config (default):"); println!(" Channel size: {}", dispatcher_config.channel_size); println!(" Priority dispatch: {}", dispatcher_config.priority_dispatch); println!(" Max frame age: {}ms", dispatcher_config.max_frame_age_ms); println!(" Backpressure enabled: {}", dispatcher_config.enable_backpressure); println!(); // Demonstrate bitmap converter creation let converter = BitmapConverter::new(1920, 1080); println!("BitmapConverter created for 1920x1080"); let stats = converter.get_statistics(); println!(" Frames converted: {}", stats.frames_converted); println!(" Bytes processed: {}", stats.bytes_processed); println!(); // Demonstrate RDP pixel formats println!("RDP Pixel Formats:"); for format in [ RdpPixelFormat::BgrX32, RdpPixelFormat::Bgr24, RdpPixelFormat::Rgb16, RdpPixelFormat::Rgb15, ] { println!(" {:?}: {} bytes/pixel", format, format.bytes_per_pixel()); } println!(); // Demonstrate rectangle operations let rect1 = Rectangle::new(0, 0, 100, 100); let rect2 = Rectangle::new(50, 50, 150, 150); println!("Rectangle operations:"); println!( " rect1: ({}, {}) to ({}, {})", rect1.left, rect1.top, rect1.right, rect1.bottom ); println!(" rect1 area: {} pixels", rect1.area()); println!( " rect2: ({}, {}) to ({}, {})", rect2.left, rect2.top, rect2.right, rect2.bottom ); println!(" Intersects: {}", rect1.intersects(&rect2)); println!(); // Demonstrate helper functions println!("Helper functions:"); println!( " Recommended queue size for 60Hz: {}", lamco_video::recommended_queue_size(60) ); println!( " Recommended queue size for 144Hz: {}", lamco_video::recommended_queue_size(144) ); println!( " RDP stride for 1920px @ BgrX32: {} bytes", lamco_video::calculate_rdp_stride(1920, RdpPixelFormat::BgrX32) ); } lamco-video-0.1.8/src/converter.rs000064400000000000000000000541171046102023000152050ustar 00000000000000//! Video Frame Converter //! //! Converts PipeWire VideoFrame structs into RDP-ready bitmap data. //! This module handles: //! - Pixel format conversion to RDP-compatible formats //! - Stride alignment for RDP protocol requirements //! - Buffer pooling for memory efficiency //! - Damage region tracking and optimization //! - SIMD-optimized conversion routines where available //! //! The converter prepares data structures ready for RDP transmission. //! When IronRDP becomes available, these will integrate seamlessly with //! IronRDP's bitmap encoding functionality. use std::sync::Arc; use std::time::Instant; use lamco_pipewire::{FfiDamageRegion, PixelFormat, VideoFrame, convert_format}; use parking_lot::RwLock; /// Alignment boundary for RDP bitmaps (64 bytes for SIMD optimization) const RDP_BITMAP_ALIGNMENT: usize = 64; /// Maximum number of pooled buffers const BUFFER_POOL_SIZE: usize = 8; /// Damage threshold for forcing full update (75% of screen) const DAMAGE_THRESHOLD: f32 = 0.75; /// RDP pixel format (subset of formats supported by RDP protocol) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RdpPixelFormat { /// 32-bit BGRX (B8G8R8X8) BgrX32, /// 24-bit BGR (B8G8R8) Bgr24, /// 16-bit RGB (R5G6B5) Rgb16, /// 15-bit RGB (X1R5G5B5) Rgb15, } impl RdpPixelFormat { /// Get bytes per pixel for this format pub fn bytes_per_pixel(&self) -> usize { match self { Self::BgrX32 => 4, Self::Bgr24 => 3, Self::Rgb16 | Self::Rgb15 => 2, } } /// Get the best RDP format for a given PipeWire format pub fn from_pixel_format(format: PixelFormat) -> Self { match format { PixelFormat::BGRA | PixelFormat::BGRx => Self::BgrX32, PixelFormat::RGBA | PixelFormat::RGBx => Self::BgrX32, PixelFormat::RGB | PixelFormat::BGR => Self::Bgr24, // YUV formats convert to BGRX32 PixelFormat::NV12 | PixelFormat::YUY2 | PixelFormat::I420 => Self::BgrX32, PixelFormat::GRAY8 => Self::BgrX32, // Expand to RGB } } } /// Rectangle representing a region #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Rectangle { pub left: u16, pub top: u16, pub right: u16, pub bottom: u16, } impl Rectangle { /// Create a new rectangle pub fn new(left: u16, top: u16, right: u16, bottom: u16) -> Self { Self { left, top, right, bottom, } } /// Get width pub fn width(&self) -> u16 { self.right.saturating_sub(self.left) } /// Get height pub fn height(&self) -> u16 { self.bottom.saturating_sub(self.top) } /// Get area pub fn area(&self) -> u32 { self.width() as u32 * self.height() as u32 } /// Check if this rectangle intersects another pub fn intersects(&self, other: &Rectangle) -> bool { !(self.right <= other.left || other.right <= self.left || self.bottom <= other.top || other.bottom <= self.top) } /// Merge with another rectangle (union) pub fn merge(&mut self, other: &Rectangle) { self.left = self.left.min(other.left); self.top = self.top.min(other.top); self.right = self.right.max(other.right); self.bottom = self.bottom.max(other.bottom); } } impl From for Rectangle { fn from(damage: FfiDamageRegion) -> Self { Self { left: damage.x as u16, top: damage.y as u16, right: ((damage.x as u32) + damage.width) as u16, bottom: ((damage.y as u32) + damage.height) as u16, } } } /// RDP-ready bitmap data #[derive(Debug, Clone)] pub struct BitmapData { /// Region this bitmap covers pub rectangle: Rectangle, /// Pixel format pub format: RdpPixelFormat, /// Raw pixel data pub data: Vec, /// Whether data is compressed pub compressed: bool, } /// Bitmap update containing one or more changed regions #[derive(Debug, Clone)] pub struct BitmapUpdate { /// Rectangles to update pub rectangles: Vec, } /// Pooled buffer for reuse #[derive(Clone)] struct PooledBuffer { data: Vec, capacity: usize, last_used: Instant, } /// Buffer pool for memory efficiency struct BufferPool { buffers: Vec>, free_indices: Vec, } impl BufferPool { fn new(size: usize) -> Self { let mut buffers = Vec::with_capacity(size); let mut free_indices = Vec::with_capacity(size); for i in 0..size { buffers.push(None); free_indices.push(i); } Self { buffers, free_indices } } /// Acquire a buffer from the pool fn acquire(&mut self, size: usize) -> Vec { // Try to find a suitable buffer in the pool for (idx, buffer_opt) in self.buffers.iter_mut().enumerate() { if let Some(buffer) = buffer_opt { if buffer.capacity >= size && self.free_indices.contains(&idx) { self.free_indices.retain(|&i| i != idx); buffer.last_used = Instant::now(); let mut data = std::mem::take(&mut buffer.data); data.clear(); data.resize(size, 0); return data; } } } // Allocate new buffer with alignment let aligned_size = align_to_boundary(size, RDP_BITMAP_ALIGNMENT); let mut buffer = Vec::with_capacity(aligned_size); buffer.resize(size, 0); buffer } /// Release a buffer back to the pool fn release(&mut self, mut buffer: Vec) { let capacity = buffer.capacity(); buffer.clear(); // Find a free slot or evict oldest if let Some(idx) = self.free_indices.pop() { self.buffers[idx] = Some(PooledBuffer { data: buffer, capacity, last_used: Instant::now(), }); } else { // Evict oldest buffer let mut oldest_idx = 0; let mut oldest_time = Instant::now(); for (idx, buffer_opt) in self.buffers.iter().enumerate() { if let Some(buf) = buffer_opt { if buf.last_used < oldest_time { oldest_time = buf.last_used; oldest_idx = idx; } } } self.buffers[oldest_idx] = Some(PooledBuffer { data: buffer, capacity, last_used: Instant::now(), }); } } } /// Damage tracker for optimizing bitmap updates struct DamageTracker { regions: Vec, full_update: bool, screen_width: u16, screen_height: u16, } impl DamageTracker { fn new(width: u16, height: u16) -> Self { Self { regions: Vec::new(), full_update: false, screen_width: width, screen_height: height, } } /// Add a damage region fn add_damage(&mut self, region: Rectangle) { if self.full_update { return; } // Check if this damage should trigger a full update let total_area = self.screen_width as u32 * self.screen_height as u32; let damage_area = region.area(); if damage_area as f32 / total_area as f32 > DAMAGE_THRESHOLD { self.full_update = true; self.regions.clear(); return; } // Try to merge with existing regions let mut merged = false; for existing in &mut self.regions { if existing.intersects(®ion) { existing.merge(®ion); merged = true; break; } } if !merged { self.regions.push(region); } // Consolidate overlapping regions self.consolidate_regions(); } /// Consolidate overlapping regions fn consolidate_regions(&mut self) { if self.regions.len() < 2 { return; } let mut consolidated = Vec::new(); let mut used = vec![false; self.regions.len()]; for i in 0..self.regions.len() { if used[i] { continue; } let mut current = self.regions[i]; used[i] = true; for j in (i + 1)..self.regions.len() { if used[j] { continue; } if current.intersects(&self.regions[j]) { current.merge(&self.regions[j]); used[j] = true; } } consolidated.push(current); } self.regions = consolidated; } /// Get damage regions to update fn get_damage_regions(&self) -> Vec { if self.full_update { vec![Rectangle::new(0, 0, self.screen_width, self.screen_height)] } else { self.regions.clone() } } /// Reset damage tracking fn reset(&mut self) { self.regions.clear(); self.full_update = false; } } /// Conversion statistics #[derive(Debug, Clone, Default)] pub struct ConversionStats { /// Total frames converted pub frames_converted: u64, /// Total bytes processed pub bytes_processed: u64, /// Total conversion time (nanoseconds) pub conversion_time_ns: u64, /// Frames with SIMD optimization pub simd_optimized_frames: u64, } impl ConversionStats { /// Get average conversion time in milliseconds pub fn avg_conversion_time_ms(&self) -> f64 { if self.frames_converted == 0 { 0.0 } else { (self.conversion_time_ns as f64 / self.frames_converted as f64) / 1_000_000.0 } } /// Get throughput in MB/s pub fn throughput_mbps(&self) -> f64 { if self.conversion_time_ns == 0 { 0.0 } else { (self.bytes_processed as f64 / 1_048_576.0) / (self.conversion_time_ns as f64 / 1_000_000_000.0) } } } /// Bitmap converter pub struct BitmapConverter { buffer_pool: Arc>, damage_tracker: Arc>, last_frame_hash: u64, enable_simd: bool, stats: Arc>, } impl BitmapConverter { /// Create a new bitmap converter pub fn new(width: u16, height: u16) -> Self { Self { buffer_pool: Arc::new(RwLock::new(BufferPool::new(BUFFER_POOL_SIZE))), damage_tracker: Arc::new(RwLock::new(DamageTracker::new(width, height))), last_frame_hash: u64::MAX, // Sentinel: ensures first frame is never skipped enable_simd: Self::detect_simd_support(), stats: Arc::new(RwLock::new(ConversionStats::default())), } } /// Detect SIMD support fn detect_simd_support() -> bool { #[cfg(target_arch = "x86_64")] { is_x86_feature_detected!("avx2") || is_x86_feature_detected!("ssse3") } #[cfg(target_arch = "aarch64")] { std::arch::is_aarch64_feature_detected!("neon") } #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { false } } /// Convert a video frame to RDP bitmap update /// /// # Arguments /// * `frame` - The video frame to convert /// /// # Returns /// A `BitmapUpdate` containing the changed regions ready for RDP transmission /// /// # Errors /// Returns an error if: /// - Frame format is unsupported /// - Frame data is invalid /// - Buffer allocation fails pub fn convert_frame(&mut self, frame: &VideoFrame) -> Result { let start_time = Instant::now(); // Validate frame if !frame.is_valid() { return Err(ConversionError::InvalidFrame( "Frame is corrupted or incomplete".to_string(), )); } // Calculate frame hash for change detection let pixel_data = frame.data().ok_or_else(|| { ConversionError::InvalidFrame("DMA-BUF frames not supported in bitmap conversion".to_string()) })?; let frame_hash = self.calculate_frame_hash(pixel_data); // Check if frame has changed if frame_hash == self.last_frame_hash { return Ok(BitmapUpdate { rectangles: vec![] }); } self.last_frame_hash = frame_hash; // Process damage regions if !frame.damage_regions.is_empty() { let mut tracker = self.damage_tracker.write(); for damage in &frame.damage_regions { tracker.add_damage(Rectangle::from(*damage)); } } else { // No damage info = full update self.damage_tracker.write().full_update = true; } let damage_regions = self.damage_tracker.read().get_damage_regions(); // Calculate output size let rdp_format = RdpPixelFormat::from_pixel_format(frame.format); let output_size = Self::calculate_output_size(frame.width, frame.height, rdp_format); // Acquire buffer from pool let mut output_buffer = self.buffer_pool.write().acquire(output_size); // Perform format conversion self.convert_frame_data(frame, &mut output_buffer, rdp_format)?; // Create bitmap data for each damage region let mut rectangles = Vec::new(); for region in damage_regions { let bitmap_data = self.create_bitmap_data(&output_buffer, region, frame.width, frame.height, rdp_format)?; rectangles.push(bitmap_data); } // Update statistics let elapsed = start_time.elapsed(); let mut stats = self.stats.write(); stats.frames_converted += 1; stats.bytes_processed += frame.data_size() as u64; stats.conversion_time_ns += elapsed.as_nanos() as u64; if self.enable_simd { stats.simd_optimized_frames += 1; } // Release buffer back to pool self.buffer_pool.write().release(output_buffer); // Reset damage tracker self.damage_tracker.write().reset(); Ok(BitmapUpdate { rectangles }) } /// Calculate frame hash for change detection fn calculate_frame_hash(&self, data: &[u8]) -> u64 { // Simple FNV-1a hash, sampling every 64th byte for speed let mut hash: u64 = 0xcbf29ce484222325; for &byte in data.iter().step_by(64) { hash ^= byte as u64; hash = hash.wrapping_mul(0x100000001b3); } hash } /// Calculate output buffer size fn calculate_output_size(width: u32, height: u32, format: RdpPixelFormat) -> usize { let stride = calculate_rdp_stride(width, format); (stride * height) as usize } /// Convert frame data to target format fn convert_frame_data( &self, frame: &VideoFrame, dst: &mut [u8], rdp_format: RdpPixelFormat, ) -> Result<(), ConversionError> { // Use lamco-pipewire format conversion as base // Then ensure RDP-compatible stride alignment let target_format = match rdp_format { RdpPixelFormat::BgrX32 => PixelFormat::BGRx, RdpPixelFormat::Bgr24 => PixelFormat::BGR, RdpPixelFormat::Rgb16 | RdpPixelFormat::Rgb15 => { return Err(ConversionError::UnsupportedFormat(frame.format)); } }; let pixel_data = frame.data().ok_or_else(|| { ConversionError::InvalidFrame("DMA-BUF frames not supported in format conversion".to_string()) })?; let dst_stride = calculate_rdp_stride(frame.width, rdp_format); convert_format( pixel_data, dst, frame.format, target_format, frame.width, frame.height, frame.stride, dst_stride, ) .map_err(|e| ConversionError::ConversionFailed(e.to_string())) } /// Create bitmap data for a specific region fn create_bitmap_data( &self, buffer: &[u8], region: Rectangle, width: u32, height: u32, format: RdpPixelFormat, ) -> Result { let region_width = region.width() as u32; let region_height = region.height() as u32; let stride = calculate_rdp_stride(width, format); let bpp = format.bytes_per_pixel() as u32; // Extract region data let mut region_data = Vec::with_capacity((region_width * region_height * bpp) as usize); for y in region.top..region.bottom { if y >= height as u16 { break; } let src_offset = (y as u32 * stride + region.left as u32 * bpp) as usize; let row_size = (region_width * bpp) as usize; if src_offset + row_size <= buffer.len() { region_data.extend_from_slice(&buffer[src_offset..src_offset + row_size]); } else { return Err(ConversionError::BufferTooSmall { required: src_offset + row_size, provided: buffer.len(), }); } } Ok(BitmapData { rectangle: region, format, data: region_data, compressed: false, }) } /// Force a full update on the next frame pub fn force_full_update(&mut self) { self.damage_tracker.write().full_update = true; } /// Get conversion statistics pub fn get_statistics(&self) -> ConversionStats { self.stats.read().clone() } /// Reset statistics pub fn reset_statistics(&mut self) { *self.stats.write() = ConversionStats::default(); } } /// Calculate RDP-compatible stride (aligned to 64 bytes) fn calculate_rdp_stride(width: u32, format: RdpPixelFormat) -> u32 { let bpp = format.bytes_per_pixel() as u32; let row_bytes = width * bpp; align_to_boundary(row_bytes as usize, RDP_BITMAP_ALIGNMENT) as u32 } /// Align value to boundary fn align_to_boundary(value: usize, boundary: usize) -> usize { (value + boundary - 1) & !(boundary - 1) } /// Conversion errors #[derive(Debug, thiserror::Error)] pub enum ConversionError { #[error("Unsupported pixel format: {0:?}")] UnsupportedFormat(PixelFormat), #[error("Buffer too small: required {required} bytes, provided {provided} bytes")] BufferTooSmall { required: usize, provided: usize }, #[error("Invalid frame: {0}")] InvalidFrame(String), #[error("Conversion failed: {0}")] ConversionFailed(String), #[error("Allocation failed for {0} bytes")] AllocationFailed(usize), } // SAFETY: BitmapConverter is Send because all its fields are Send: // - Arc> is Send when BufferPool is Send (see below) // - Arc> is Send when DamageTracker is Send (see below) // - u64 is Send // - bool is Send // - Arc> is Send when ConversionStats is Send (derives) #[allow(unsafe_code)] unsafe impl Send for BitmapConverter {} // SAFETY: BufferPool is Send because all its fields are Send: // - Vec> is Send when PooledBuffer is Send // - PooledBuffer contains Vec, usize, Instant - all Send // - Vec is Send #[allow(unsafe_code)] unsafe impl Send for BufferPool {} // SAFETY: DamageTracker is Send because all its fields are Send: // - Vec is Send (Rectangle is Copy and contains only primitives) // - bool, u16 are Send #[allow(unsafe_code)] unsafe impl Send for DamageTracker {} #[cfg(test)] mod tests { use super::*; #[test] fn test_rectangle_operations() { let rect1 = Rectangle::new(0, 0, 100, 100); let rect2 = Rectangle::new(50, 50, 150, 150); assert_eq!(rect1.width(), 100); assert_eq!(rect1.height(), 100); assert_eq!(rect1.area(), 10000); assert!(rect1.intersects(&rect2)); let mut merged = rect1; merged.merge(&rect2); assert_eq!(merged, Rectangle::new(0, 0, 150, 150)); } #[test] fn test_damage_tracker() { let mut tracker = DamageTracker::new(1920, 1080); // Add non-overlapping regions tracker.add_damage(Rectangle::new(0, 0, 100, 100)); tracker.add_damage(Rectangle::new(200, 200, 300, 300)); assert_eq!(tracker.regions.len(), 2); // Add overlapping region - should merge tracker.add_damage(Rectangle::new(50, 50, 150, 150)); assert_eq!(tracker.regions.len(), 2); tracker.reset(); assert_eq!(tracker.regions.len(), 0); } #[test] fn test_buffer_pool() { let mut pool = BufferPool::new(4); let buf1 = pool.acquire(1024); assert_eq!(buf1.len(), 1024); let buf2 = pool.acquire(2048); assert_eq!(buf2.len(), 2048); pool.release(buf1); let buf3 = pool.acquire(1024); assert_eq!(buf3.len(), 1024); } #[test] fn test_rdp_pixel_format() { assert_eq!(RdpPixelFormat::BgrX32.bytes_per_pixel(), 4); assert_eq!(RdpPixelFormat::Bgr24.bytes_per_pixel(), 3); assert_eq!(RdpPixelFormat::Rgb16.bytes_per_pixel(), 2); assert_eq!( RdpPixelFormat::from_pixel_format(PixelFormat::BGRA), RdpPixelFormat::BgrX32 ); } #[test] fn test_stride_calculation() { assert_eq!(calculate_rdp_stride(1920, RdpPixelFormat::BgrX32), 7680); assert_eq!(calculate_rdp_stride(1921, RdpPixelFormat::BgrX32), 7744); // Aligned assert_eq!(calculate_rdp_stride(1920, RdpPixelFormat::Bgr24), 5760); } #[test] fn test_conversion_stats() { let mut stats = ConversionStats::default(); stats.frames_converted = 100; stats.bytes_processed = 100_000_000; stats.conversion_time_ns = 500_000_000; // 500ms assert_eq!(stats.avg_conversion_time_ms(), 5.0); // 5ms per frame assert!(stats.throughput_mbps() > 0.0); } #[test] fn test_bitmap_converter_creation() { let converter = BitmapConverter::new(1920, 1080); let stats = converter.get_statistics(); assert_eq!(stats.frames_converted, 0); } } lamco-video-0.1.8/src/dispatcher.rs000064400000000000000000000371241046102023000153230ustar 00000000000000//! Frame Dispatcher //! //! Routes video frames from multiple PipeWire streams to frame processors. //! Handles: //! - Multi-stream coordination //! - Priority-based frame processing //! - Backpressure management //! - Load balancing across monitors //! - Frame drop decisions based on system load use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::{Duration, Instant}; use lamco_pipewire::VideoFrame; use parking_lot::RwLock; use tokio::sync::mpsc; use tracing::{debug, error, trace, warn}; /// Default channel buffer size const DEFAULT_CHANNEL_SIZE: usize = 30; /// Maximum frame age before forced drop (milliseconds) const MAX_FRAME_AGE_MS: u64 = 150; /// High water mark for backpressure (percentage of queue) const HIGH_WATER_MARK: f32 = 0.8; /// Low water mark for backpressure release (percentage of queue) const LOW_WATER_MARK: f32 = 0.5; /// Dispatcher configuration #[derive(Debug, Clone)] pub struct DispatcherConfig { /// Channel buffer size per stream pub channel_size: usize, /// Enable priority-based dispatch pub priority_dispatch: bool, /// Maximum frame age before drop (ms) pub max_frame_age_ms: u64, /// Enable backpressure handling pub enable_backpressure: bool, /// High water mark (0.0-1.0) pub high_water_mark: f32, /// Low water mark (0.0-1.0) pub low_water_mark: f32, /// Enable load balancing pub load_balancing: bool, } impl Default for DispatcherConfig { fn default() -> Self { Self { channel_size: DEFAULT_CHANNEL_SIZE, priority_dispatch: true, max_frame_age_ms: MAX_FRAME_AGE_MS, enable_backpressure: true, high_water_mark: HIGH_WATER_MARK, low_water_mark: LOW_WATER_MARK, load_balancing: true, } } } /// Dispatcher statistics #[derive(Debug, Clone, Default)] pub struct DispatcherStats { /// Total frames received pub frames_received: u64, /// Total frames dispatched pub frames_dispatched: u64, /// Frames dropped due to age pub frames_dropped_age: u64, /// Frames dropped due to backpressure pub frames_dropped_backpressure: u64, /// Current active streams pub active_streams: usize, /// Total dispatch time (nanoseconds) pub total_dispatch_time_ns: u64, /// Backpressure active pub backpressure_active: bool, } impl DispatcherStats { /// Get average dispatch time in microseconds pub fn avg_dispatch_time_us(&self) -> f64 { if self.frames_dispatched == 0 { 0.0 } else { (self.total_dispatch_time_ns as f64 / self.frames_dispatched as f64) / 1_000.0 } } /// Get drop rate pub fn drop_rate(&self) -> f64 { if self.frames_received == 0 { 0.0 } else { let total_drops = self.frames_dropped_age + self.frames_dropped_backpressure; total_drops as f64 / self.frames_received as f64 } } /// Get dispatch rate pub fn dispatch_rate(&self) -> f64 { if self.frames_received == 0 { 0.0 } else { self.frames_dispatched as f64 / self.frames_received as f64 } } } /// Stream priority #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum StreamPriority { Low = 0, Normal = 1, High = 2, } /// Frame with dispatch metadata struct DispatchFrame { frame: VideoFrame, priority: StreamPriority, enqueue_time: Instant, } impl DispatchFrame { fn new(frame: VideoFrame, priority: StreamPriority) -> Self { Self { frame, priority, enqueue_time: Instant::now(), } } fn age(&self) -> Duration { self.enqueue_time.elapsed() } fn is_too_old(&self, max_age_ms: u64) -> bool { self.age().as_millis() as u64 > max_age_ms } } /// Per-stream state struct StreamState { priority: StreamPriority, frame_count: u64, last_frame_time: Option, backpressure_active: bool, } impl StreamState { fn new(priority: StreamPriority) -> Self { Self { priority, frame_count: 0, last_frame_time: None, backpressure_active: false, } } fn update_frame_received(&mut self) { self.frame_count += 1; self.last_frame_time = Some(Instant::now()); } } /// Frame dispatcher pub struct FrameDispatcher { config: DispatcherConfig, streams: Arc>>, priority_queue: Arc>>, stats: Arc>, running: Arc>, } impl FrameDispatcher { /// Create a new frame dispatcher /// /// # Arguments /// * `config` - Dispatcher configuration /// /// # Returns /// A new `FrameDispatcher` instance pub fn new(config: DispatcherConfig) -> Self { Self { config, streams: Arc::new(RwLock::new(HashMap::new())), priority_queue: Arc::new(RwLock::new(VecDeque::new())), stats: Arc::new(RwLock::new(DispatcherStats::default())), running: Arc::new(RwLock::new(false)), } } /// Register a stream /// /// # Arguments /// * `stream_id` - Unique stream identifier (monitor index) /// * `priority` - Stream priority pub fn register_stream(&self, stream_id: u32, priority: StreamPriority) { self.streams.write().insert(stream_id, StreamState::new(priority)); debug!("Registered stream {} with priority {:?}", stream_id, priority); } /// Unregister a stream /// /// # Arguments /// * `stream_id` - Stream identifier to remove pub fn unregister_stream(&self, stream_id: u32) { self.streams.write().remove(&stream_id); debug!("Unregistered stream {}", stream_id); } /// Start dispatching frames /// /// # Arguments /// * `input` - Receiver for incoming frames from all streams /// * `output` - Sender for dispatched frames /// /// # Returns /// An async task handle /// /// # Errors /// Returns an error if dispatcher fails to start pub async fn start( self: Arc, mut input: mpsc::Receiver, output: mpsc::Sender, ) -> Result<(), DispatchError> { *self.running.write() = true; debug!("Frame dispatcher started"); while *self.running.read() { // Process incoming frames match input.recv().await { Some(frame) => { self.handle_incoming_frame(frame).await; } None => { debug!("Input channel closed, stopping dispatcher"); break; } } // Dispatch queued frames self.dispatch_frames(&output).await?; } *self.running.write() = false; Ok(()) } /// Stop the dispatcher pub fn stop(&self) { *self.running.write() = false; } /// Handle an incoming frame async fn handle_incoming_frame(&self, frame: VideoFrame) { let start_time = Instant::now(); // Update stats self.stats.write().frames_received += 1; // Get stream state let stream_id = frame.monitor_index; let priority = { let mut streams = self.streams.write(); let state = streams .entry(stream_id) .or_insert_with(|| StreamState::new(StreamPriority::Normal)); state.update_frame_received(); // Check backpressure if self.config.enable_backpressure { let queue = self.priority_queue.read(); let queue_usage = queue.len() as f32 / self.config.channel_size as f32; if !state.backpressure_active && queue_usage >= self.config.high_water_mark { state.backpressure_active = true; self.stats.write().backpressure_active = true; warn!( "Backpressure activated for stream {} (queue usage: {:.1}%)", stream_id, queue_usage * 100.0 ); } else if state.backpressure_active && queue_usage <= self.config.low_water_mark { state.backpressure_active = false; self.stats.write().backpressure_active = false; debug!( "Backpressure released for stream {} (queue usage: {:.1}%)", stream_id, queue_usage * 100.0 ); } // Drop frame if backpressure active if state.backpressure_active { trace!( "Dropping frame {} from stream {} due to backpressure", frame.frame_id, stream_id ); self.stats.write().frames_dropped_backpressure += 1; return; } } state.priority }; // Create dispatch frame let dispatch_frame = DispatchFrame::new(frame, priority); // Add to priority queue self.enqueue_frame(dispatch_frame); // Update dispatch time let elapsed = start_time.elapsed(); self.stats.write().total_dispatch_time_ns += elapsed.as_nanos() as u64; } /// Enqueue a frame in priority order fn enqueue_frame(&self, frame: DispatchFrame) { let mut queue = self.priority_queue.write(); if self.config.priority_dispatch { // Insert based on priority (higher priority first) let mut insert_idx = queue.len(); for (idx, queued) in queue.iter().enumerate() { if frame.priority > queued.priority { insert_idx = idx; break; } } queue.insert(insert_idx, frame); } else { // FIFO order queue.push_back(frame); } // Update active streams count let active_streams = self.streams.read().len(); self.stats.write().active_streams = active_streams; } /// Dispatch frames from the queue async fn dispatch_frames(&self, output: &mpsc::Sender) -> Result<(), DispatchError> { let mut queue = self.priority_queue.write(); // Process all available frames while let Some(dispatch_frame) = queue.pop_front() { // Check frame age if dispatch_frame.is_too_old(self.config.max_frame_age_ms) { trace!( "Dropping old frame {} (age: {:?})", dispatch_frame.frame.frame_id, dispatch_frame.age() ); self.stats.write().frames_dropped_age += 1; continue; } // Dispatch frame match output.try_send(dispatch_frame.frame.clone()) { Ok(_) => { trace!( "Dispatched frame {} with priority {:?}", dispatch_frame.frame.frame_id, dispatch_frame.priority ); self.stats.write().frames_dispatched += 1; } Err(mpsc::error::TrySendError::Full(_)) => { // Put frame back and stop dispatching warn!("Output channel full, requeueing frame"); queue.push_front(dispatch_frame); break; } Err(mpsc::error::TrySendError::Closed(_)) => { error!("Output channel closed"); return Err(DispatchError::ChannelClosed); } } } Ok(()) } /// Get dispatcher statistics pub fn get_statistics(&self) -> DispatcherStats { self.stats.read().clone() } /// Reset statistics pub fn reset_statistics(&self) { let mut stats = self.stats.write(); *stats = DispatcherStats::default(); } /// Check if dispatcher is running pub fn is_running(&self) -> bool { *self.running.read() } /// Get active stream count pub fn active_stream_count(&self) -> usize { self.streams.read().len() } /// Get queue depth pub fn queue_depth(&self) -> usize { self.priority_queue.read().len() } } /// Dispatch errors #[derive(Debug, thiserror::Error)] pub enum DispatchError { #[error("Channel closed")] ChannelClosed, #[error("Stream {0} not found")] StreamNotFound(u32), #[error("Queue overflow: {0} frames")] QueueOverflow(usize), #[error("Dispatcher not running")] NotRunning, #[error("Invalid priority: {0}")] InvalidPriority(String), } #[cfg(test)] mod tests { use lamco_pipewire::PixelFormat; use super::*; #[test] fn test_dispatcher_config() { let config = DispatcherConfig::default(); assert_eq!(config.channel_size, DEFAULT_CHANNEL_SIZE); assert!(config.priority_dispatch); assert!(config.enable_backpressure); } #[test] fn test_dispatcher_stats() { let mut stats = DispatcherStats::default(); stats.frames_received = 100; stats.frames_dispatched = 90; stats.frames_dropped_age = 5; stats.frames_dropped_backpressure = 5; assert_eq!(stats.drop_rate(), 0.1); assert_eq!(stats.dispatch_rate(), 0.9); } #[test] fn test_stream_priority() { assert!(StreamPriority::High > StreamPriority::Normal); assert!(StreamPriority::Normal > StreamPriority::Low); } #[test] fn test_dispatch_frame() { let frame = VideoFrame::new(1, 1920, 1080, 7680, PixelFormat::BGRA, 0); let dispatch = DispatchFrame::new(frame, StreamPriority::High); assert_eq!(dispatch.priority, StreamPriority::High); assert!(!dispatch.is_too_old(MAX_FRAME_AGE_MS)); } #[test] fn test_dispatcher_creation() { let config = DispatcherConfig::default(); let dispatcher = FrameDispatcher::new(config); assert!(!dispatcher.is_running()); assert_eq!(dispatcher.active_stream_count(), 0); assert_eq!(dispatcher.queue_depth(), 0); } #[test] fn test_stream_registration() { let config = DispatcherConfig::default(); let dispatcher = FrameDispatcher::new(config); dispatcher.register_stream(0, StreamPriority::High); assert_eq!(dispatcher.active_stream_count(), 1); dispatcher.register_stream(1, StreamPriority::Normal); assert_eq!(dispatcher.active_stream_count(), 2); dispatcher.unregister_stream(0); assert_eq!(dispatcher.active_stream_count(), 1); } #[tokio::test] async fn test_dispatcher_lifecycle() { let config = DispatcherConfig::default(); let dispatcher = Arc::new(FrameDispatcher::new(config)); let (input_tx, input_rx) = mpsc::channel(10); let (output_tx, _output_rx) = mpsc::channel(10); // Start dispatcher let dispatcher_clone = dispatcher.clone(); let handle = tokio::spawn(async move { dispatcher_clone.start(input_rx, output_tx).await }); // Give it a moment to start, then stop tokio::time::sleep(Duration::from_millis(10)).await; dispatcher.stop(); // Drop input_tx to close the channel and unblock the dispatcher drop(input_tx); // Wait for completion with timeout let result = tokio::time::timeout(Duration::from_millis(100), handle).await; assert!(result.is_ok()); } } lamco-video-0.1.8/src/lib.rs000064400000000000000000000200121046102023000137270ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] //! # lamco-video //! //! Video frame processing and RDP bitmap conversion for Wayland screen capture. //! //! This crate is part of the [lamco-wayland](https://github.com/lamco-admin/lamco-wayland) //! workspace and is designed to work with [`lamco-pipewire`](https://crates.io/crates/lamco-pipewire) //! for video frame processing. //! //! # Features //! //! - **Frame Processing Pipeline**: Configurable video frame processing with rate limiting //! - **RDP Bitmap Conversion**: Convert PipeWire frames to RDP-ready bitmap format //! - **Damage Region Tracking**: Optimize updates by only sending changed regions //! - **Buffer Pooling**: Efficient memory management with reusable buffers //! - **Priority-Based Dispatch**: Multi-stream coordination with backpressure handling //! - **SIMD Optimization**: Automatic use of SIMD instructions where available //! //! # Requirements //! //! This crate requires: //! - **Linux** with a Wayland compositor //! - **Rust 1.77+** //! - **lamco-pipewire** for frame capture //! //! # Quick Start //! //! ```rust,ignore //! use lamco_video::{FrameProcessor, ProcessorConfig, BitmapConverter}; //! use lamco_pipewire::VideoFrame; //! use tokio::sync::mpsc; //! //! // Create frame processor //! let config = ProcessorConfig::default(); //! let processor = std::sync::Arc::new(FrameProcessor::new(config, 1920, 1080)); //! //! // Create channels //! let (input_tx, input_rx) = mpsc::channel(30); //! let (output_tx, mut output_rx) = mpsc::channel(30); //! //! // Start processor //! let processor_clone = processor.clone(); //! tokio::spawn(async move { //! processor_clone.start(input_rx, output_tx).await //! }); //! //! // Send frames from lamco-pipewire to processor //! // Receive processed bitmap updates //! while let Some(bitmap_update) = output_rx.recv().await { //! for rect in &bitmap_update.rectangles { //! println!("Update region: {:?}", rect.rectangle); //! } //! } //! ``` //! //! # Architecture //! //! The processing pipeline: //! //! ```text //! ┌────────────────────┐ //! │ lamco-pipewire │ //! │ (VideoFrame) │ //! └─────────┬──────────┘ //! │ //! ▼ //! ┌────────────────────┐ //! │ FrameDispatcher │ ◄── Multi-stream routing //! │ (priority queue) │ Backpressure handling //! └─────────┬──────────┘ //! │ //! ▼ //! ┌────────────────────┐ //! │ FrameProcessor │ ◄── Frame rate limiting //! │ (rate control) │ Age-based dropping //! └─────────┬──────────┘ //! │ //! ▼ //! ┌────────────────────┐ //! │ BitmapConverter │ ◄── Pixel format conversion //! │ (format conv) │ Damage region tracking //! └─────────┬──────────┘ Buffer pooling //! │ //! ▼ //! ┌────────────────────┐ //! │ BitmapUpdate │ ◄── RDP-ready rectangles //! │ (RDP output) │ //! └────────────────────┘ //! ``` //! //! # Configuration //! //! ## Processor Configuration //! //! ```rust //! use lamco_video::ProcessorConfig; //! //! let config = ProcessorConfig { //! target_fps: 60, // Target frame rate //! max_queue_depth: 30, // Max frames in queue before dropping //! adaptive_quality: true, // Enable adaptive quality //! damage_threshold: 0.05, // Minimum damage area to process (5%) //! drop_on_full_queue: true, // Drop frames when queue is full //! enable_metrics: true, // Enable statistics collection //! }; //! ``` //! //! ## Dispatcher Configuration //! //! ```rust //! use lamco_video::DispatcherConfig; //! //! let config = DispatcherConfig { //! channel_size: 30, // Buffer size per stream //! priority_dispatch: true, // Enable priority-based dispatch //! max_frame_age_ms: 150, // Drop frames older than 150ms //! enable_backpressure: true, // Enable backpressure handling //! high_water_mark: 0.8, // Trigger backpressure at 80% //! low_water_mark: 0.5, // Release backpressure at 50% //! load_balancing: true, // Enable load balancing //! }; //! ``` //! //! # RDP Pixel Formats //! //! The converter supports these RDP-compatible output formats: //! //! | Format | BPP | Description | //! |--------|-----|-------------| //! | BgrX32 | 4 | 32-bit BGRX (most common) | //! | Bgr24 | 3 | 24-bit BGR | //! | Rgb16 | 2 | 16-bit RGB 5:6:5 | //! | Rgb15 | 2 | 15-bit RGB 5:5:5 | //! //! # Performance //! //! Typical performance on modern hardware: //! //! - **Conversion latency**: < 1ms per frame (1080p) //! - **Memory usage**: < 50MB (with buffer pooling) //! - **Throughput**: > 200 MB/s (with SIMD) //! - **Frame rates**: Tested up to 144Hz //! //! # Cargo Features //! //! ```toml //! [dependencies] //! lamco-video = { version = "0.1", features = ["full"] } //! ``` //! //! | Feature | Default | Description | //! |---------|---------|-------------| //! | `damage` | No | Full damage region tracking | //! | `full` | No | All features enabled | // ============================================================================= // CORE MODULES // ============================================================================= pub mod converter; pub mod dispatcher; pub mod processor; // ============================================================================= // RE-EXPORTS - PRIMARY API // ============================================================================= // Converter types pub use converter::{ BitmapConverter, BitmapData, BitmapUpdate, ConversionError, ConversionStats, RdpPixelFormat, Rectangle, }; // Dispatcher types pub use dispatcher::{DispatchError, DispatcherConfig, DispatcherStats, FrameDispatcher, StreamPriority}; // Processor types pub use processor::{FrameProcessor, ProcessingError, ProcessingStats, ProcessorConfig}; // ============================================================================= // CRATE-LEVEL ITEMS // ============================================================================= /// Crate version pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Recommended frame queue size for a given refresh rate /// /// Returns the recommended channel buffer size to hold approximately /// 500ms of frames, capped at 72 frames. /// /// # Arguments /// /// * `refresh_rate` - Monitor refresh rate in Hz /// /// # Returns /// /// Recommended channel buffer size (15-72) #[must_use] pub fn recommended_queue_size(refresh_rate: u32) -> usize { // Half second of frames, capped at 72 ((refresh_rate / 2) as usize).clamp(15, 72) } /// Calculate RDP-compatible stride for a given width and format /// /// RDP requires stride aligned to 64 bytes for optimal performance. /// /// # Arguments /// /// * `width` - Image width in pixels /// * `format` - Target RDP pixel format /// /// # Returns /// /// Aligned stride in bytes #[must_use] pub fn calculate_rdp_stride(width: u32, format: RdpPixelFormat) -> u32 { let bpp = format.bytes_per_pixel() as u32; let row_bytes = width * bpp; // Align to 64 bytes (row_bytes + 63) & !63 } #[cfg(test)] mod tests { use super::*; #[test] fn test_recommended_queue_size() { assert_eq!(recommended_queue_size(30), 15); assert_eq!(recommended_queue_size(60), 30); assert_eq!(recommended_queue_size(144), 72); assert_eq!(recommended_queue_size(240), 72); // Capped at 72 } #[test] fn test_calculate_rdp_stride() { assert_eq!(calculate_rdp_stride(1920, RdpPixelFormat::BgrX32), 7680); assert_eq!(calculate_rdp_stride(1921, RdpPixelFormat::BgrX32), 7744); } #[test] fn test_version() { assert!(!VERSION.is_empty()); } } lamco-video-0.1.8/src/processor.rs000064400000000000000000000333761046102023000152210ustar 00000000000000//! Frame Processing Pipeline //! //! Handles video frame processing with features including: //! - Frame rate control and adaptive quality //! - Frame queue management //! - Damage region optimization //! - Performance monitoring //! - Backpressure handling //! //! The processor sits between PipeWire capture and the bitmap converter, //! managing the flow of frames and adapting to system load. use std::sync::Arc; use std::time::{Duration, Instant}; use lamco_pipewire::VideoFrame; use parking_lot::RwLock; use tokio::sync::mpsc; use tracing::{debug, trace, warn}; use crate::converter::{BitmapConverter, BitmapUpdate}; /// Default frame queue size const DEFAULT_QUEUE_SIZE: usize = 30; /// Default target frame rate (FPS) const DEFAULT_TARGET_FPS: u32 = 30; /// Maximum frame age before dropping (milliseconds) const MAX_FRAME_AGE_MS: u64 = 100; /// Configuration for frame processor #[derive(Debug, Clone)] pub struct ProcessorConfig { /// Target frame rate (FPS) pub target_fps: u32, /// Maximum frame queue depth pub max_queue_depth: usize, /// Enable adaptive quality pub adaptive_quality: bool, /// Damage tracking threshold (0.0-1.0) pub damage_threshold: f32, /// Drop frames when queue is full pub drop_on_full_queue: bool, /// Enable performance metrics pub enable_metrics: bool, } impl Default for ProcessorConfig { fn default() -> Self { Self { target_fps: DEFAULT_TARGET_FPS, max_queue_depth: DEFAULT_QUEUE_SIZE, adaptive_quality: true, damage_threshold: 0.05, drop_on_full_queue: true, enable_metrics: true, } } } /// Frame processing statistics #[derive(Debug, Clone, Default)] pub struct ProcessingStats { /// Total frames received pub frames_received: u64, /// Frames processed successfully pub frames_processed: u64, /// Frames dropped due to queue full pub frames_dropped_queue_full: u64, /// Frames dropped due to age pub frames_dropped_old: u64, /// Frames skipped due to no changes pub frames_skipped_no_change: u64, /// Total processing time (nanoseconds) pub total_processing_time_ns: u64, /// Current queue depth pub current_queue_depth: usize, /// Peak queue depth pub peak_queue_depth: usize, } impl ProcessingStats { /// Get average processing time in milliseconds pub fn avg_processing_time_ms(&self) -> f64 { if self.frames_processed == 0 { 0.0 } else { (self.total_processing_time_ns as f64 / self.frames_processed as f64) / 1_000_000.0 } } /// Get drop rate pub fn drop_rate(&self) -> f64 { if self.frames_received == 0 { 0.0 } else { let total_drops = self.frames_dropped_queue_full + self.frames_dropped_old; total_drops as f64 / self.frames_received as f64 } } /// Get current FPS pub fn current_fps(&self) -> f64 { if self.total_processing_time_ns == 0 { 0.0 } else { (self.frames_processed as f64 * 1_000_000_000.0) / self.total_processing_time_ns as f64 } } } /// Frame with metadata struct QueuedFrame { frame: VideoFrame, enqueue_time: Instant, } impl QueuedFrame { fn new(frame: VideoFrame) -> Self { Self { frame, enqueue_time: Instant::now(), } } fn age(&self) -> Duration { self.enqueue_time.elapsed() } fn is_too_old(&self, max_age_ms: u64) -> bool { self.age().as_millis() as u64 > max_age_ms } } /// Frame processor pub struct FrameProcessor { config: ProcessorConfig, converter: Arc>, stats: Arc>, last_frame_time: Arc>>, running: Arc>, } impl FrameProcessor { /// Create a new frame processor /// /// # Arguments /// * `config` - Processor configuration /// * `width` - Frame width in pixels /// * `height` - Frame height in pixels /// /// # Returns /// A new `FrameProcessor` instance pub fn new(config: ProcessorConfig, width: u16, height: u16) -> Self { Self { config, converter: Arc::new(RwLock::new(BitmapConverter::new(width, height))), stats: Arc::new(RwLock::new(ProcessingStats::default())), last_frame_time: Arc::new(RwLock::new(None)), running: Arc::new(RwLock::new(false)), } } /// Start processing frames /// /// # Arguments /// * `input` - Receiver for incoming video frames /// * `output` - Sender for processed bitmap updates /// /// # Returns /// An async task handle /// /// # Errors /// Returns an error if the processor fails to start pub async fn start( self: Arc, mut input: mpsc::Receiver, output: mpsc::Sender, ) -> Result<(), ProcessingError> { *self.running.write() = true; debug!("Frame processor started with target {} FPS", self.config.target_fps); while *self.running.read() { // Wait for next frame match input.recv().await { Some(frame) => { // Update queue depth stats (use scope to ensure lock is released) let queue_depth = input.len(); { let mut stats = self.stats.write(); stats.current_queue_depth = queue_depth; if queue_depth > stats.peak_queue_depth { stats.peak_queue_depth = queue_depth; } stats.frames_received += 1; } // Check if queue is too full if queue_depth >= self.config.max_queue_depth && self.config.drop_on_full_queue { warn!( "Frame queue full ({} frames), dropping frame {}", queue_depth, frame.frame_id ); self.stats.write().frames_dropped_queue_full += 1; continue; } // Wrap frame with metadata let queued_frame = QueuedFrame::new(frame); // Check frame age if queued_frame.is_too_old(MAX_FRAME_AGE_MS) { trace!( "Dropping old frame {} (age: {:?})", queued_frame.frame.frame_id, queued_frame.age() ); self.stats.write().frames_dropped_old += 1; continue; } // Apply frame rate limiting if !self.should_process_frame(&queued_frame.frame) { continue; } // Process the frame match self.process_frame(queued_frame.frame).await { Ok(bitmap_update) => { // Skip frames with no changes if bitmap_update.rectangles.is_empty() { self.stats.write().frames_skipped_no_change += 1; continue; } // Send processed frame if let Err(e) = output.send(bitmap_update).await { warn!("Failed to send bitmap update: {}", e); break; } self.stats.write().frames_processed += 1; } Err(e) => { warn!("Frame processing error: {}", e); continue; } } } None => { debug!("Input channel closed, stopping processor"); break; } } } *self.running.write() = false; Ok(()) } /// Stop the processor pub fn stop(&self) { *self.running.write() = false; } /// Check if we should process this frame based on frame rate limiting fn should_process_frame(&self, _frame: &VideoFrame) -> bool { let mut last_time = self.last_frame_time.write(); // Always process first frame if last_time.is_none() { *last_time = Some(Instant::now()); return true; } let last = last_time.unwrap(); let elapsed = last.elapsed(); // Calculate minimum interval based on target FPS let min_interval = Duration::from_nanos(1_000_000_000 / self.config.target_fps as u64); if elapsed >= min_interval { *last_time = Some(Instant::now()); true } else { false } } /// Process a single frame async fn process_frame(&self, frame: VideoFrame) -> Result { let start_time = Instant::now(); trace!( "Processing frame {} ({}x{}, format: {:?})", frame.frame_id, frame.width, frame.height, frame.format ); // Check if frame has significant damage if !frame.damage_regions.is_empty() { let has_damage = frame.has_significant_damage(self.config.damage_threshold); if !has_damage { trace!("Frame {} has insignificant damage, skipping", frame.frame_id); return Ok(BitmapUpdate { rectangles: vec![] }); } } // Convert frame let bitmap_update = self .converter .write() .convert_frame(&frame) .map_err(|e| ProcessingError::ConversionFailed(e.to_string()))?; // Update processing time stats let elapsed = start_time.elapsed(); self.stats.write().total_processing_time_ns += elapsed.as_nanos() as u64; trace!("Frame {} processed in {:?}", frame.frame_id, elapsed); Ok(bitmap_update) } /// Get processing statistics pub fn get_statistics(&self) -> ProcessingStats { self.stats.read().clone() } /// Reset statistics pub fn reset_statistics(&self) { let mut stats = self.stats.write(); *stats = ProcessingStats::default(); } /// Get converter statistics pub fn get_converter_statistics(&self) -> crate::converter::ConversionStats { self.converter.read().get_statistics() } /// Force full update on next frame pub fn force_full_update(&self) { self.converter.write().force_full_update(); } /// Check if processor is running pub fn is_running(&self) -> bool { *self.running.read() } } /// Processing errors #[derive(Debug, thiserror::Error)] pub enum ProcessingError { #[error("Conversion failed: {0}")] ConversionFailed(String), #[error("Queue overflow: max depth {0} exceeded")] QueueOverflow(usize), /// Invalid frame data or format #[error("Invalid frame: {0}")] InvalidFrame(String), /// Channel communication error #[error("Channel error: {0}")] ChannelError(String), /// Processor is not currently running #[error("Processor not running")] NotRunning, } #[cfg(test)] mod tests { use lamco_pipewire::PixelFormat; use super::*; #[test] fn test_processor_config() { let config = ProcessorConfig::default(); assert_eq!(config.target_fps, DEFAULT_TARGET_FPS); assert_eq!(config.max_queue_depth, DEFAULT_QUEUE_SIZE); assert!(config.adaptive_quality); } #[test] fn test_processing_stats() { let mut stats = ProcessingStats::default(); stats.frames_received = 100; stats.frames_processed = 90; stats.frames_dropped_queue_full = 5; stats.frames_dropped_old = 5; stats.total_processing_time_ns = 500_000_000; // 500ms assert_eq!(stats.drop_rate(), 0.1); // 10% drop rate // Use approximate comparison for floating-point let avg_time = stats.avg_processing_time_ms(); let expected = 500.0 / 90.0; // ~5.556ms per frame assert!((avg_time - expected).abs() < 1e-10); } #[test] fn test_queued_frame() { let frame = VideoFrame::new(1, 1920, 1080, 7680, PixelFormat::BGRA, 0); let queued = QueuedFrame::new(frame); assert!(!queued.is_too_old(MAX_FRAME_AGE_MS)); assert!(queued.age() < Duration::from_millis(10)); } #[tokio::test] async fn test_processor_creation() { let config = ProcessorConfig::default(); let processor = Arc::new(FrameProcessor::new(config, 1920, 1080)); assert!(!processor.is_running()); let stats = processor.get_statistics(); assert_eq!(stats.frames_received, 0); } #[tokio::test] async fn test_processor_lifecycle() { let config = ProcessorConfig::default(); let processor = Arc::new(FrameProcessor::new(config, 1920, 1080)); let (input_tx, input_rx) = mpsc::channel(10); let (output_tx, _output_rx) = mpsc::channel(10); // Start processor let processor_clone = processor.clone(); let handle = tokio::spawn(async move { processor_clone.start(input_rx, output_tx).await }); // Give it a moment to start, then stop tokio::time::sleep(Duration::from_millis(10)).await; processor.stop(); // Drop input_tx to close the channel and unblock the processor drop(input_tx); // Wait for completion with timeout let result = tokio::time::timeout(Duration::from_millis(100), handle).await; assert!(result.is_ok()); } }