diff --git a/Cargo.lock b/Cargo.lock index abd3ff0..7129ea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aligned" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" dependencies = [ "as-slice", ] @@ -101,13 +101,11 @@ version = "0.0.3" dependencies = [ "asimov-module", "bytes", - "cfg-if", "clap", "clientele", + "crossbeam-channel", "ctrlc", - "derive_more", "dispatch2", - "dogma", "image", "image_hasher", "know", @@ -119,16 +117,15 @@ dependencies = [ "objc2-core-media", "objc2-core-video", "objc2-foundation", - "scopeguard", "serde_json", "thiserror 2.0.17", ] [[package]] name = "asimov-env" -version = "25.0.2" +version = "25.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59240068f0c558026d60bc1ff0af5d6ad5039f096a0ddd2ac8478b3aa70d0bf5" +checksum = "66b413eb1dc1f71e823e14216535ca894f34a763211c179b9238d83dbbca7c85" dependencies = [ "cap-directories", "cap-std", @@ -138,9 +135,9 @@ dependencies = [ [[package]] name = "asimov-module" -version = "25.0.2" +version = "25.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf5d03eec28785668aa7d8a83c65a21ff87552cf402fed80c4efa28c57284bb" +checksum = "715a172b8b9c2f2cf87ad08a774a98ffb22073970d0226ee65672fdc2a819236" dependencies = [ "asimov-env", "clientele", @@ -245,9 +242,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.8.1" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +checksum = "234655ec178edd82b891e262ea7cf71f6584bcd09eff94db786be23f1821825c" dependencies = [ "bon-macros", "rustversion", @@ -255,11 +252,11 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.8.1" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -276,9 +273,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -300,9 +297,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" [[package]] name = "cap-directories" @@ -349,9 +346,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -396,9 +393,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -406,9 +403,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstyle", "clap_lex", @@ -493,6 +490,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -541,8 +547,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -559,13 +575,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -586,27 +626,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "directories-next" version = "2.0.0" @@ -752,9 +771,9 @@ dependencies = [ [[package]] name = "fast_image_resize" -version = "5.4.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049915d74c5dfae375a3f5bf54f47ed593ba27b29f792617a9a987521d56d674" +checksum = "6b6e793dfd0ee192d1999c655797ecc956c82d1f6d367be20bf6b81d6a1c87ac" dependencies = [ "bytemuck", "cfg-if", @@ -795,9 +814,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" @@ -879,9 +898,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -1013,9 +1032,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1027,9 +1046,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1094,7 +1113,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.0", - "zune-jpeg 0.5.5", + "zune-jpeg 0.5.8", ] [[package]] @@ -1140,9 +1159,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1185,9 +1204,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1204,15 +1223,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -1224,9 +1243,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -1261,9 +1280,9 @@ dependencies = [ [[package]] name = "know" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5167fd8fd5fa17dd08fc69ed7ff3fdeb56caabe65106da7c66bd93ce438f292" +checksum = "3409ed93f31d3240975ee49351a676d3ace4ff3d29ba9e669687995752e39f1c" dependencies = [ "base64", "cfg_eval", @@ -1310,9 +1329,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libfuzzer-sys" @@ -1326,9 +1345,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", @@ -1354,9 +1373,9 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -1407,9 +1426,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -1529,6 +1548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -1659,6 +1679,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -1782,9 +1811,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -1840,9 +1869,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -1868,9 +1897,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -1892,9 +1921,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2075,9 +2104,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -2102,12 +2131,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "schemars" version = "0.9.0" @@ -2122,9 +2145,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -2132,12 +2155,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "secrecy" version = "0.10.3" @@ -2185,16 +2202,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2207,9 +2224,9 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -2222,7 +2239,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn", @@ -2245,9 +2262,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -2290,9 +2307,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2436,9 +2453,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2446,9 +2463,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2493,17 +2510,11 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2905,18 +2916,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -2956,9 +2967,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -2998,6 +3009,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" + [[package]] name = "zune-core" version = "0.4.12" @@ -3030,9 +3047,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" +checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" dependencies = [ "zune-core 0.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index 0448a67..feb087a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,30 +15,27 @@ license = "Unlicense" keywords = ["asimov-module", "asimov", "ai"] categories = ["command-line-utilities", "text-processing"] # TODO publish = false +build = "build.rs" [features] -# Supported today: CLI + std + ffmpeg desktop backend. -default = ["cli", "std", "ffmpeg"] - -# Experimental backends (compile-gated, may be stubbed at runtime). -experimental = ["android", "avf", "dshow", "v4l2"] - -# Backward-compatible alias. "native" currently means "experimental native backends". -native = ["experimental"] - -# "all" means: everything we can compile & wire up today (not necessarily fully implemented). -all = ["ffmpeg", "pretty", "tracing", "experimental"] - -cli = ["asimov-module/cli", "std", "dep:clap", "dep:clientele"] +default = ["cli", "std", "ffmpeg", "android", "avf"] +cli = [ + "asimov-module/cli", + "std", + "dep:clap", + "dep:clientele", + "dep:image", + "dep:image_hasher", + "dep:serde_json" +] std = ["asimov-module/std", "clap?/std", "clientele?/std"] -unstable = [] - pretty = [] tracing = ["asimov-module/tracing", "clientele?/tracing"] - +mobile-preview = [] ffmpeg = [] -android = ["dep:ndk-sys"] +android = ["mobile-preview", "dep:ndk-sys"] avf = [ + "mobile-preview", "dep:dispatch2", "dep:objc2", "dep:objc2-av-foundation", @@ -47,29 +44,21 @@ avf = [ "dep:objc2-core-video", "dep:objc2-foundation", ] -dshow = [] -v4l2 = [] [dependencies] -# IMPORTANT: keep std enabled for asimov-module; it currently uses std in its implementation. asimov-module = { version = "25", default-features = false, features = ["std"] } - ctrlc = "3.5" -derive_more = { version = "2", features = ["display", "error", "from"] } -dogma = { version = "0.1", features = ["traits"] } -image = "0.25" -image_hasher = { version = "3", features = ["fast_image_resize"] } -know = { version = "0.2", features = ["serde"] } -#nokhwa = { version = "0.10", features = ["input-native"] } -scopeguard = { version = "1.2", default-features = false } -serde_json = "1" thiserror = "2" bytes = "1" -cfg-if = "1" +crossbeam-channel = "0.5" +know = { version = "0.2", features = ["serde"] } -# Optional integrations: +# Optional CLI-only dependencies: clap = { version = "4.5", default-features = false, features = ["std"], optional = true } clientele = { version = "0.3.8", default-features = false, features = ["clap", "std"], optional = true } +serde_json = { version = "1.0.145", optional = true } +image = { version = "0.25", optional = true } +image_hasher = { version = "3", features = ["fast_image_resize"], optional = true } [target.'cfg(unix)'.dependencies] libc = "0.2" @@ -79,7 +68,7 @@ ndk-sys = { version = "0.6", optional = true } [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] dispatch2 = { version = "0.3", optional = true } -objc2 = { version = "0.6", optional = true } +objc2 = { version = "0.6", optional = true, features = ["exception"] } objc2-av-foundation = { version = "0.3", features = ["objc2-core-media"], optional = true } objc2-core-foundation = { version = "0.3", optional = true } objc2-core-media = { version = "0.3", optional = true } @@ -93,10 +82,10 @@ lto = "thin" [[bin]] name = "asimov-camera-reader" -path = "src/reader/main.rs" +path = "src/bin/reader.rs" required-features = ["cli"] [[bin]] name = "asimov-camera-cataloger" -path = "src/cataloger/main.rs" +path = "src/bin/cataloger.rs" required-features = ["cli"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..1084685 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") { + println!("cargo:rustc-link-lib=camera2ndk"); + println!("cargo:rustc-link-lib=mediandk"); + + println!("cargo:rustc-link-lib=android"); + println!("cargo:rustc-link-lib=log"); + } +} diff --git a/src/api/backend.rs b/src/api/backend.rs new file mode 100644 index 0000000..87d9fba --- /dev/null +++ b/src/api/backend.rs @@ -0,0 +1,8 @@ +// This is free and unencumbered software released into the public domain. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CameraBackend { + Android, + Avf, + Ffmpeg, +} diff --git a/src/api/config.rs b/src/api/config.rs new file mode 100644 index 0000000..74599f0 --- /dev/null +++ b/src/api/config.rs @@ -0,0 +1,198 @@ +// This is free and unencumbered software released into the public domain. + +#[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] +use crate::api::AndroidPreviewTarget; + +use crate::{CameraError, DeviceInfo, FrameRef}; + +use crossbeam_channel as ch; + +#[derive(Clone, Debug)] +pub struct CameraConfig { + pub device: Option, + pub width: u32, + pub height: u32, + pub fps: f64, + pub buffer_raw: usize, + pub buffer_frames: usize, + pub throttle_fps: Option, + pub diagnostics: bool, + pub frame_tx: Option>, + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + pub android_preview: AndroidPreviewTarget, +} + +impl CameraConfig { + pub fn builder() -> CameraConfigBuilder { + CameraConfigBuilder::new() + } + + pub fn normalized(mut self) -> Self { + self.width = self.width.max(1); + self.height = self.height.max(1); + + self.fps = if self.fps.is_finite() && self.fps > 0.0 { + self.fps + } else { + 30.0 + }; + + self.buffer_raw = self.buffer_raw.max(1); + self.buffer_frames = self.buffer_frames.max(1); + + self.throttle_fps = self.throttle_fps.filter(|x| x.is_finite() && *x > 0.0); + + self + } + + pub fn validate(&self) -> Result<(), CameraError> { + if self.width == 0 || self.height == 0 { + return Err(CameraError::invalid_config("width/height must be > 0")); + } + if !self.fps.is_finite() || self.fps <= 0.0 { + return Err(CameraError::invalid_config("fps must be finite and > 0")); + } + if self.buffer_raw == 0 { + return Err(CameraError::invalid_config("buffer_raw must be >= 1")); + } + if self.buffer_frames == 0 { + return Err(CameraError::invalid_config("buffer_frames must be >= 1")); + } + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct CameraConfigBuilder { + device: Option, + width: u32, + height: u32, + fps: f64, + buffer_raw: usize, + buffer_frames: usize, + throttle_fps: Option, + diagnostics: bool, + frame_tx: Option>, + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + android_preview: Option, +} + +impl CameraConfigBuilder { + fn new() -> Self { + Self { + device: None, + width: 1280, + height: 720, + fps: 30.0, + buffer_raw: 2, + buffer_frames: 1, + throttle_fps: None, + diagnostics: false, + frame_tx: None, + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + android_preview: None, + } + } + + pub fn device(mut self, device: DeviceInfo) -> Self { + self.device = Some(device); + self + } + + pub fn width(mut self, width: u32) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: u32) -> Self { + self.height = height; + self + } + + pub fn fps(mut self, fps: f64) -> Self { + self.fps = fps; + self + } + + pub fn buffer_raw(mut self, n: usize) -> Self { + self.buffer_raw = n; + self + } + + pub fn buffer_frames(mut self, n: usize) -> Self { + self.buffer_frames = n; + self + } + + pub fn throttle_fps(mut self, fps: Option) -> Self { + self.throttle_fps = fps; + self + } + + pub fn diagnostics(mut self, enabled: bool) -> Self { + self.diagnostics = enabled; + self + } + + pub fn frame_tx(mut self, tx: ch::Sender) -> Self { + self.frame_tx = Some(tx); + self + } + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + pub fn android_preview(mut self, target: AndroidPreviewTarget) -> Self { + self.android_preview = Some(target); + self + } + + pub fn build(self) -> Result { + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + { + let android_preview = self.android_preview.ok_or_else(|| { + CameraError::invalid_config( + "android_preview is required when building with mobile-preview on Android", + ) + })?; + + let cfg = CameraConfig { + device: self.device, + width: self.width, + height: self.height, + fps: self.fps, + buffer_raw: self.buffer_raw, + buffer_frames: self.buffer_frames, + throttle_fps: self.throttle_fps, + diagnostics: self.diagnostics, + frame_tx: self.frame_tx, + android_preview, + }; + + let cfg = cfg.normalized(); + cfg.validate()?; + return Ok(cfg); + } + + #[cfg(not(all(feature = "mobile-preview", feature = "android", target_os = "android")))] + { + let cfg = CameraConfig { + device: self.device, + width: self.width, + height: self.height, + fps: self.fps, + buffer_raw: self.buffer_raw, + buffer_frames: self.buffer_frames, + throttle_fps: self.throttle_fps, + diagnostics: self.diagnostics, + frame_tx: self.frame_tx, + }; + + let cfg = cfg.normalized(); + cfg.validate()?; + Ok(cfg) + } + } +} diff --git a/src/api/devices.rs b/src/api/devices.rs new file mode 100644 index 0000000..b7c4ce6 --- /dev/null +++ b/src/api/devices.rs @@ -0,0 +1,130 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraError, drivers}; + +#[derive(Clone, Debug)] +pub struct DeviceInfo { + id: String, + name: String, + kind: DeviceKind, +} + +impl DeviceInfo { + pub(crate) fn new(id: String, name: String, kind: DeviceKind) -> Self { + Self { id, name, kind } + } + pub fn id(&self) -> &str { + &self.id + } + pub fn name(&self) -> &str { + &self.name + } + pub fn kind(&self) -> DeviceKind { + self.kind + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DeviceKind { + External, + Front, + Back, + Unknown, +} + +/// Public: list devices for the current platform/backend selection. +pub fn list_video_devices() -> Result, CameraError> { + let mut devices = list_video_devices_impl()?; + + devices.retain(|d| !is_screen_capture_like(d)); + + Ok(devices) +} + +pub fn pick_preferred_device(devices: &[DeviceInfo]) -> Option { + if devices.is_empty() { + return None; + } + + if let Some(d) = devices + .iter() + .find(|d| d.kind() == DeviceKind::External && !is_continuity_like(d)) + { + return Some(d.clone()); + } + + if let Some(d) = devices.iter().find(|d| d.kind() == DeviceKind::Front) { + return Some(d.clone()); + } + + if let Some(d) = devices.iter().find(|d| d.kind() == DeviceKind::Back) { + return Some(d.clone()); + } + + if let Some(d) = devices + .iter() + .find(|d| d.kind() == DeviceKind::External && is_continuity_like(d)) + { + return Some(d.clone()); + } + + Some(devices[0].clone()) +} + +pub fn default_device() -> Result, CameraError> { + let devices = list_video_devices()?; + Ok(pick_preferred_device(&devices)) +} + +fn is_screen_capture_like(d: &DeviceInfo) -> bool { + let name = d.name().trim().to_ascii_lowercase(); + let id = d.id().trim().to_ascii_lowercase(); + + name.contains("capture screen") + || name.starts_with("capture screen") + || name.contains("screen capture") + || name.starts_with("screen ") + || id.contains("capture screen") + || id.contains("screen capture") +} + +fn is_continuity_like(d: &DeviceInfo) -> bool { + let name = d.name().trim().to_ascii_lowercase(); + + name.contains("iphone") + || name.contains("ipad") + || name.contains("desk view") + || name.contains("continuity") +} + +#[cfg(all(feature = "android", target_os = "android"))] +fn list_video_devices_impl() -> Result, CameraError> { + drivers::android::devices::list_video_devices() +} + +#[cfg(all(feature = "avf", any(target_os = "ios", target_os = "macos")))] +fn list_video_devices_impl() -> Result, CameraError> { + drivers::avf::devices::list_video_devices() +} + +#[cfg(all( + feature = "ffmpeg", + any(target_os = "macos", target_os = "windows", target_os = "linux"), + not(all(target_os = "macos", feature = "avf")), +))] +fn list_video_devices_impl() -> Result, CameraError> { + drivers::ffmpeg::devices::list_video_devices() +} + +#[cfg(not(any( + all(feature = "android", target_os = "android"), + all(feature = "avf", any(target_os = "ios", target_os = "macos")), + all( + feature = "ffmpeg", + any(target_os = "macos", target_os = "windows", target_os = "linux"), + not(all(target_os = "macos", feature = "avf")), + ), +)))] +fn list_video_devices_impl() -> Result, CameraError> { + Ok(Vec::new()) +} diff --git a/src/api/error.rs b/src/api/error.rs new file mode 100644 index 0000000..9f65569 --- /dev/null +++ b/src/api/error.rs @@ -0,0 +1,114 @@ +// This is free and unencumbered software released into the public domain. + +use std::{error::Error as StdError, sync::Arc}; + +pub type DynError = Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CameraErrorKind { + NoDriver, + NotApplicable, + NoCamera, + NotConfigured, + Unsupported, + InvalidConfig, + Closed, + Driver, + Other, +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum CameraError { + #[error("no camera driver available")] + NoDriver, + + #[error("driver not applicable")] + NotApplicable, + + #[error("no camera device available")] + NoCamera, + + #[error("camera not configured")] + NotConfigured, + + #[error("unsupported: {0}")] + Unsupported(String), + + #[error("invalid config: {0}")] + InvalidConfig(String), + + #[error("camera is closed")] + Closed, + + #[error("{context}: {source}")] + Driver { + context: &'static str, + #[source] + source: DynError, + }, + + #[error("{0}")] + Other(String), +} + +impl CameraError { + #[inline] + pub const fn kind(&self) -> CameraErrorKind { + match self { + Self::NoDriver => CameraErrorKind::NoDriver, + Self::NotApplicable => CameraErrorKind::NotApplicable, + Self::NoCamera => CameraErrorKind::NoCamera, + Self::NotConfigured => CameraErrorKind::NotConfigured, + Self::Unsupported(_) => CameraErrorKind::Unsupported, + Self::InvalidConfig(_) => CameraErrorKind::InvalidConfig, + Self::Closed => CameraErrorKind::Closed, + Self::Driver { .. } => CameraErrorKind::Driver, + Self::Other(_) => CameraErrorKind::Other, + } + } + + #[inline] + pub fn driver(context: &'static str, source: impl StdError + Send + Sync + 'static) -> Self { + Self::Driver { + context, + source: Arc::new(source), + } + } + + #[inline] + pub fn unsupported(msg: impl Into) -> Self { + Self::Unsupported(msg.into()) + } + + #[inline] + pub fn invalid_config(msg: impl Into) -> Self { + Self::InvalidConfig(msg.into()) + } + + #[inline] + pub fn other(msg: impl Into) -> Self { + Self::Other(msg.into()) + } + + #[inline] + pub const fn is_not_applicable(&self) -> bool { + matches!(self, Self::NotApplicable) + } + + #[inline] + pub const fn is_expected(&self) -> bool { + matches!( + self, + Self::NotApplicable | Self::NoDriver | Self::NoCamera | Self::Closed + ) + } +} + +impl From for CameraError { + fn from(e: DynError) -> Self { + CameraError::Driver { + context: "error", + source: e, + } + } +} diff --git a/src/api/frame.rs b/src/api/frame.rs new file mode 100644 index 0000000..33ef6c7 --- /dev/null +++ b/src/api/frame.rs @@ -0,0 +1,157 @@ +// This is free and unencumbered software released into the public domain. + +use bytes::Bytes; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PixelFormat { + /// Packed RGB, 8-bit per channel, 3 bytes per pixel. + Rgb8, + /// Packed BGRA, 8-bit per channel, 4 bytes per pixel. + Bgra8, +} + +impl PixelFormat { + #[inline] + pub const fn bytes_per_pixel_packed(self) -> Option { + match self { + PixelFormat::Rgb8 => Some(3), + PixelFormat::Bgra8 => Some(4), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RawFormat { + AndroidYuv420888, + PackedRgb8, + PackedBgra8, +} + +#[derive(Clone, Debug)] +pub struct RawPlane { + pub data: Bytes, + pub row_stride: u32, + pub pixel_stride: u32, +} + +impl RawPlane { + #[inline] + pub fn new(data: Bytes, row_stride: u32, pixel_stride: u32) -> Self { + Self { + data, + row_stride, + pixel_stride, + } + } +} + +#[derive(Clone, Debug)] +pub struct RawFrame { + pub width: u32, + pub height: u32, + pub format: RawFormat, + pub planes: Vec, + pub timestamp_ns: Option, +} + +impl RawFrame { + #[inline] + pub fn new( + width: u32, + height: u32, + format: RawFormat, + planes: Vec, + timestamp_ns: Option, + ) -> Self { + Self { + width, + height, + format, + planes, + timestamp_ns, + } + } + + #[inline] + pub fn new_rgb8( + width: u32, + height: u32, + data: Vec, + row_stride: u32, + timestamp_ns: Option, + ) -> Self { + RawFrame::new( + width, + height, + RawFormat::PackedRgb8, + vec![RawPlane::new(Bytes::from(data), row_stride, 3)], + timestamp_ns, + ) + } + + #[inline] + pub fn new_bgra8( + width: u32, + height: u32, + data: Vec, + row_stride: u32, + timestamp_ns: Option, + ) -> Self { + RawFrame::new( + width, + height, + RawFormat::PackedBgra8, + vec![RawPlane::new(Bytes::from(data), row_stride, 4)], + timestamp_ns, + ) + } +} + +#[derive(Clone, Debug)] +pub struct Frame { + pub width: u32, + pub height: u32, + + /// Bytes between successive rows for packed formats (Rgb8/Bgra8). + pub stride: u32, + + pub pixel_format: PixelFormat, + + /// Packed pixel bytes. For `Rgb8`: len ~= stride*height. For `Bgra8`: same. + pub data: Bytes, + + pub timestamp_ns: Option, +} + +impl Frame { + #[inline] + pub fn new( + width: u32, + height: u32, + stride: u32, + pixel_format: PixelFormat, + data: Bytes, + timestamp_ns: Option, + ) -> Self { + Self { + width, + height, + stride, + pixel_format, + data, + timestamp_ns, + } + } + + #[inline] + pub fn packed_len_expected(&self) -> Option { + let bpp = self.pixel_format.bytes_per_pixel_packed()? as usize; + let min_stride = (self.width as usize).saturating_mul(bpp); + let stride = (self.stride as usize).max(min_stride); + Some(stride.saturating_mul(self.height as usize)) + } +} + +pub type RawFrameRef = Arc; +pub type FrameRef = Arc; diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..661b94e --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,26 @@ +// This is free and unencumbered software released into the public domain. + +mod backend; +pub use backend::*; + +mod config; +pub use config::*; + +mod devices; +pub use devices::*; + +mod error; +pub use error::*; + +mod frame; +pub use frame::*; + +#[cfg(all( + feature = "mobile-preview", + any(target_os = "ios", target_os = "android") +))] +mod preview; +#[cfg(all(feature = "mobile-preview", target_os = "android"))] +pub use preview::AndroidPreviewTarget; +#[cfg(all(feature = "mobile-preview", target_os = "ios"))] +pub use preview::AvfSessionHandle; diff --git a/src/api/preview.rs b/src/api/preview.rs new file mode 100644 index 0000000..d89e0e5 --- /dev/null +++ b/src/api/preview.rs @@ -0,0 +1,51 @@ +// This is free and unencumbered software released into the public domain. + +#![cfg(all( + feature = "mobile-preview", + any(target_os = "ios", target_os = "android") +))] + +use core::ffi::c_void; +use core::ptr::NonNull; + +pub mod handles { + use super::*; + + #[cfg(all(feature = "android", target_os = "android"))] + #[derive(Clone, Copy, Debug)] + pub struct AndroidPreviewTarget(pub NonNull); + + #[cfg(all(feature = "android", target_os = "android"))] + impl AndroidPreviewTarget { + pub unsafe fn from_nonnull_ptr(ptr: NonNull) -> Self { + Self(ptr) + } + + #[inline] + pub fn as_ptr(self) -> *mut c_void { + self.0.as_ptr() + } + } + + #[cfg(all(feature = "avf", target_os = "ios"))] + #[derive(Clone, Copy, Debug)] + pub struct AvfSessionHandle(pub NonNull); + + #[cfg(all(feature = "avf", target_os = "ios"))] + impl AvfSessionHandle { + pub unsafe fn from_nonnull_ptr(ptr: NonNull) -> Self { + Self(ptr) + } + + #[inline] + pub fn as_ptr(self) -> *mut c_void { + self.0.as_ptr() + } + } +} + +#[cfg(all(feature = "android", target_os = "android"))] +pub use handles::AndroidPreviewTarget; + +#[cfg(all(feature = "avf", target_os = "ios"))] +pub use handles::AvfSessionHandle; diff --git a/src/cataloger/main.rs b/src/bin/cataloger.rs similarity index 73% rename from src/cataloger/main.rs rename to src/bin/cataloger.rs index 63eff4a..e0782dc 100644 --- a/src/cataloger/main.rs +++ b/src/bin/cataloger.rs @@ -3,7 +3,7 @@ #[cfg(not(feature = "std"))] compile_error!("asimov-camera-cataloger requires the 'std' feature"); -use asimov_camera_module::{cli, shared::CameraError}; +use asimov_camera_module::{CameraError, DeviceKind}; use asimov_module::SysexitsError::{self, *}; use clap::Parser; use clientele::StandardOptions; @@ -62,7 +62,7 @@ fn run_cataloger(options: &Options) -> Result<(), CameraError> { eprintln!("INFO: enumerating camera devices"); } - let mut devices = cli::list_video_devices(&options.flags)?; + let mut devices = asimov_camera_module::list_video_devices()?; if devices.is_empty() { if options.flags.debug || options.flags.verbose >= 1 { eprintln!("WARN: no camera devices found"); @@ -70,19 +70,28 @@ fn run_cataloger(options: &Options) -> Result<(), CameraError> { return Ok(()); } - devices.sort_by(|a, b| a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name))); + devices.sort_by(|a, b| a.id().cmp(b.id()).then_with(|| a.name().cmp(b.name()))); for d in devices { + let kind = kind_label(d.kind()); + match options.output { OutputFormat::Text => { - if d.is_usb { - println!("{}: {} [usb]", d.id, d.name); - } else { - println!("{}: {}", d.id, d.name); + println!("{}: {}", d.id(), d.name()); + + if options.flags.debug || options.flags.verbose >= 2 { + eprintln!("DEBUG: device id={} kind={kind} name={}", d.id(), d.name()); } }, OutputFormat::Jsonl => { - println!("{}", json!({ "id": d.id, "name": d.name, "usb": d.is_usb })); + println!( + "{}", + json!({ + "id": d.id(), + "name": d.name(), + "kind": kind, + }) + ); }, } } @@ -90,6 +99,15 @@ fn run_cataloger(options: &Options) -> Result<(), CameraError> { Ok(()) } +fn kind_label(k: DeviceKind) -> &'static str { + match k { + DeviceKind::External => "external", + DeviceKind::Front => "front", + DeviceKind::Back => "back", + DeviceKind::Unknown => "unknown", + } +} + fn handle_error(err: &CameraError, flags: &StandardOptions) -> SysexitsError { use std::error::Error as _; use std::io::Write; @@ -111,7 +129,7 @@ fn handle_error(err: &CameraError, flags: &StandardOptions) -> SysexitsError { CameraError::NotConfigured => EX_CONFIG, CameraError::InvalidConfig(_) => EX_USAGE, CameraError::Unsupported(_) => EX_UNAVAILABLE, - CameraError::DriverError { .. } => EX_SOFTWARE, + CameraError::Driver { .. } => EX_SOFTWARE, CameraError::Other(_) => EX_SOFTWARE, _ => EX_SOFTWARE, } diff --git a/src/reader/main.rs b/src/bin/reader.rs similarity index 62% rename from src/reader/main.rs rename to src/bin/reader.rs index 6b8f487..ddddd8c 100644 --- a/src/reader/main.rs +++ b/src/bin/reader.rs @@ -4,14 +4,15 @@ compile_error!("asimov-camera-reader requires the 'std' feature"); use asimov_camera_module::{ - cli, - shared::{CameraConfig, CameraError, CameraEvent, Frame, PixelFormat, open_camera}, + CameraConfig, CameraError, DeviceInfo, DeviceKind, FrameRef, PixelFormat, default_device, + list_video_devices, open_camera, }; use asimov_module::SysexitsError::{self, *}; use clap::Parser; use clientele::StandardOptions; use image_hasher::{HashAlg, HasherConfig}; use know::traits::ToJsonLd; + use std::{ error::Error as StdError, io::{self, Write}, @@ -74,14 +75,11 @@ pub fn main() -> Result> { fn run_reader(opts: &Options) -> Result<(), CameraError> { if opts.list_devices { - let mut devices = cli::list_video_devices(&opts.flags)?; - devices.sort_by(|a, b| a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name))); + let mut devices = list_video_devices()?; + devices.sort_by(|a, b| a.id().cmp(b.id()).then_with(|| a.name().cmp(b.name()))); for d in devices { - if d.is_usb { - println!("{}: {} [usb]", d.id, d.name); - } else { - println!("{}: {}", d.id, d.name); - } + let _kind: DeviceKind = d.kind(); + println!("{}: {}", d.id(), d.name()); } return Ok(()); } @@ -102,34 +100,66 @@ fn run_reader(opts: &Options) -> Result<(), CameraError> { let fps = opts.frequency.max(0.1); let min_interval = Duration::from_secs_f64(1.0 / fps); - let device_id = cli::auto_select_device(&opts.flags, opts.device.clone())? - .unwrap_or_else(default_device_for_platform); + let device = resolve_device(opts.device.as_deref())?; + + let device_id_cb: String = match (opts.device.as_deref(), device.as_ref()) { + (Some(raw), _) => raw.trim().to_string(), + (None, Some(dev)) => dev.id().to_string(), + (None, None) => "auto".to_string(), + }; + + let cfg = { + let mut b = CameraConfig::builder() + .width(width) + .height(height) + .fps(fps) + .diagnostics(debug || verbose >= 2); + + if let Some(dev) = device.clone() { + b = b.device(dev); + } + + b.build()? + }; + + let mut cam = open_camera(cfg)?; + + if debug || verbose >= 1 { + let label = opts + .device + .as_deref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .unwrap_or_else(|| device_id_cb.clone()); + + eprintln!("INFO: opening camera device={label}"); + } - let config = CameraConfig::new(width, height, fps) - .with_device(device_id.clone()) - .with_diagnostics(debug || verbose >= 2); + cam.start()?; + let rx = cam.read_frames()?; let last_emit = Arc::new(Mutex::new(Instant::now())); let last_hash: Arc>> = Arc::new(Mutex::new(None)); let hasher = (opts.debounce > 0).then(|| HasherConfig::new().hash_alg(HashAlg::Gradient).to_hasher()); - let quit_cb = Arc::clone(&quit); - let last_emit_cb = Arc::clone(&last_emit); - let last_hash_cb = Arc::clone(&last_hash); - let debounce_level = opts.debounce; - let device_id_cb = device_id.clone(); + while !quit.load(Ordering::SeqCst) { + let frame: FrameRef = match rx.recv_timeout(Duration::from_millis(250)) { + Ok(f) => f, + Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue, + Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break, + }; - let callback = Arc::new(move |frame: Frame| { - if quit_cb.load(Ordering::SeqCst) { - return; + if quit.load(Ordering::SeqCst) { + break; } { - let mut guard = last_emit_cb.lock().unwrap_or_else(|p| p.into_inner()); + let mut guard = last_emit.lock().unwrap_or_else(|p| p.into_inner()); let now = Instant::now(); if now.duration_since(*guard) < min_interval { - return; + continue; } *guard = now; } @@ -144,10 +174,10 @@ fn run_reader(opts: &Options) -> Result<(), CameraError> { let img_data = image::DynamicImage::ImageRgb8(img_buffer); let hash = hasher.hash_image(&img_data); - let mut prev = last_hash_cb.lock().unwrap_or_else(|p| p.into_inner()); + let mut prev = last_hash.lock().unwrap_or_else(|p| p.into_inner()); if let Some(ref mut prev_hash) = *prev { - if hash.dist(prev_hash) < debounce_level as u32 { - return; + if hash.dist(prev_hash) < opts.debounce as u32 { + continue; } *prev_hash = hash; } else { @@ -157,17 +187,15 @@ fn run_reader(opts: &Options) -> Result<(), CameraError> { } } - let ts_ns: u64 = if frame.timestamp_ns != 0 { - frame.timestamp_ns - } else { + let ts_ns: u64 = frame.timestamp_ns.unwrap_or(0).max({ SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_nanos() as u64) .unwrap_or(0) - }; + }); let img = know::classes::Image { - id: Some(format!("{device_id_cb}#{ts_ns}")), + id: Some(format!("{}#{}", device_id_cb, ts_ns)), width: Some(frame.width as _), height: Some(frame.height as _), data: frame.data.to_vec(), @@ -176,92 +204,37 @@ fn run_reader(opts: &Options) -> Result<(), CameraError> { let json = match img.to_jsonld() { Ok(v) => v, - Err(_) => return, + Err(_) => continue, }; let mut out = io::stdout().lock(); if let Err(err) = writeln!(&mut out, "{json}") { if err.kind() == io::ErrorKind::BrokenPipe { - quit_cb.store(true, Ordering::SeqCst); + quit.store(true, Ordering::SeqCst); } } - }); - - let mut cam = open_camera("", config)?; - cam.add_sink(callback); - - if debug || verbose >= 1 { - eprintln!("INFO: opening camera device={device_id}"); - } - - cam.start()?; - - while !quit.load(Ordering::SeqCst) { - if debug || verbose >= 1 { - drain_events(cam.events(), debug, verbose); - } - std::thread::sleep(Duration::from_millis(50)); } let _ = cam.stop(); Ok(()) } -fn drain_events(rx: &std::sync::mpsc::Receiver, debug: bool, verbose: u8) { - loop { - match rx.try_recv() { - Ok(ev) => print_event(ev, debug, verbose), - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => break, - } - } -} +fn resolve_device(id_opt: Option<&str>) -> Result, CameraError> { + let id = id_opt.map(|s| s.trim()).filter(|s| !s.is_empty()); -fn print_event(ev: CameraEvent, debug: bool, verbose: u8) { - match ev { - CameraEvent::Started { backend } => { - if debug || verbose >= 1 { - eprintln!("INFO: camera started ({backend:?})"); - } - }, - CameraEvent::Stopped { backend } => { - if debug || verbose >= 1 { - eprintln!("INFO: camera stopped ({backend:?})"); - } - }, - CameraEvent::FrameDropped { backend } => { - if debug || verbose >= 2 { - eprintln!("WARN: frame dropped ({backend:?})"); - } - }, - CameraEvent::Warning { backend, message } => { - if debug || verbose >= 1 { - eprintln!("WARN: {backend:?}: {message}"); - } - }, - CameraEvent::Error { backend, error } => { - eprintln!("ERROR: {backend:?}: {error}"); - }, + if id.is_none() { + return Ok(default_device()?); } -} -fn default_device_for_platform() -> String { - #[cfg(target_os = "macos")] - { - "avf:0".to_string() - } - #[cfg(target_os = "linux")] - { - "file:/dev/video0".to_string() - } - #[cfg(target_os = "windows")] - { - "dshow:video=default".to_string() - } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - "file:/dev/video0".to_string() + let want = id.unwrap(); + let devices = list_video_devices()?; + if let Some(d) = devices.into_iter().find(|d| d.id() == want) { + return Ok(Some(d)); } + + Err(CameraError::invalid_config(format!( + "unknown device id '{want}'; run with --list-devices" + ))) } fn parse_dimensions(s: &str) -> Result<(u32, u32), String> { diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..ccc9523 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,231 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{ + CameraBackend, CameraConfig, CameraError, Frame, FrameRef, PixelFormat, RawFormat, RawFrameRef, + default_device, drivers, +}; + +use crate::converter; +use crate::runtime::sampler::FpsSampler; + +use crossbeam_channel as ch; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +pub struct Camera { + driver: Box, + frame_rx: ch::Receiver, + stop_tx: ch::Sender<()>, + worker: Option>, + closed: Arc, +} + +impl Camera { + pub fn backend(&self) -> CameraBackend { + self.driver.backend() + } + + pub fn start(&mut self) -> Result<(), CameraError> { + self.driver.start() + } + + pub fn stop(&mut self) -> Result<(), CameraError> { + self.driver.stop() + } + + pub fn close(&mut self) -> Result<(), CameraError> { + if self.closed.swap(true, Ordering::AcqRel) { + return Ok(()); + } + + let _ = self.stop_tx.try_send(()); + if let Some(h) = self.worker.take() { + let _ = h.join(); + } + + let _ = self.stop(); + self.driver.close() + } + + pub fn read_frames(&mut self) -> Result, CameraError> { + if self.closed.load(Ordering::Acquire) { + return Err(CameraError::Closed); + } + Ok(self.frame_rx.clone()) + } + + pub fn driver_preview_info(&self) -> Option<(u32, u32, i32)> { + self.driver.preview_info() + } + + fn raw_to_frame(raw: RawFrameRef) -> Option { + let r = raw.as_ref(); + if r.planes.is_empty() { + return None; + } + + match r.format { + RawFormat::PackedRgb8 => { + let p0 = &r.planes[0]; + + if p0.pixel_stride != 3 { + return None; + } + + Some(Arc::new(Frame::new( + r.width, + r.height, + p0.row_stride.max(r.width.saturating_mul(3)), + PixelFormat::Rgb8, + p0.data.clone(), + r.timestamp_ns, + ))) + }, + + _ => converter::convert_raw_to_frame(raw, PixelFormat::Rgb8), + } + } +} + +impl Drop for Camera { + fn drop(&mut self) { + let _ = self.close(); + } +} + +pub fn open_camera(mut cfg: CameraConfig) -> Result { + cfg = cfg.normalized(); + + if cfg.device.is_none() { + cfg.device = default_device()?; + } + if cfg.device.is_none() { + return Err(CameraError::NoCamera); + } + + cfg.validate()?; + + let driver_cfg = build_driver_config(&cfg)?; + let mut driver = drivers::open(&driver_cfg)?; + + let raw_rx = driver.read_frames()?; + + let cap = cfg.buffer_frames.max(1); + let (frame_tx, frame_rx) = ch::bounded::(cap); + let frame_rx_thread = frame_rx.clone(); + + let (stop_tx, stop_rx) = ch::bounded::<()>(1); + + let throttle_fps = cfg.throttle_fps; + let diagnostics = cfg.diagnostics; + let extra_frame_tx = cfg.frame_tx.clone(); + + let closed = Arc::new(AtomicBool::new(false)); + let closed_thread = Arc::clone(&closed); + + let worker = std::thread::Builder::new() + .name("asimov-camera-dispatch".to_string()) + .spawn(move || { + let mut sampler = throttle_fps.map(FpsSampler::new); + + loop { + ch::select! { + recv(stop_rx) -> _ => break, + + recv(raw_rx) -> msg => { + let mut raw = match msg { + Ok(v) => v, + Err(_) => break, + }; + + while let Ok(next) = raw_rx.try_recv() { + raw = next; + } + + if closed_thread.load(Ordering::Relaxed) { + continue; + } + + if let Some(s) = sampler.as_mut() { + if !s.should_emit() { + continue; + } + } + + let Some(frame) = Camera::raw_to_frame(raw) else { + continue; + }; + + if frame_tx.try_send(frame.clone()).is_err() { + let _ = frame_rx_thread.try_recv(); + let _ = frame_tx.try_send(frame.clone()); + } + + if let Some(tx) = extra_frame_tx.as_ref() { + let _ = tx.try_send(frame); + } + + let _ = diagnostics; + } + } + } + }) + .map_err(|e| CameraError::other(format!("failed to spawn camera worker: {e}")))?; + + Ok(Camera { + driver, + frame_rx, + stop_tx, + worker: Some(worker), + closed, + }) +} + +fn build_driver_config(cfg: &CameraConfig) -> Result { + let device = cfg + .device + .clone() + .ok_or_else(|| CameraError::invalid_config("device must be resolved before driver open"))?; + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + { + let dc = drivers::DriverConfig { + device, + width: cfg.width, + height: cfg.height, + fps: cfg.fps, + buffer_raw: cfg.buffer_raw, + diagnostics: cfg.diagnostics, + android_preview: cfg.android_preview, + } + .normalized(); + + dc.validate()?; + return Ok(dc); + } + + #[cfg(not(all(feature = "mobile-preview", feature = "android", target_os = "android")))] + { + let dc = drivers::DriverConfig { + device, + width: cfg.width, + height: cfg.height, + fps: cfg.fps, + buffer_raw: cfg.buffer_raw, + diagnostics: cfg.diagnostics, + } + .normalized(); + + dc.validate()?; + Ok(dc) + } +} + +#[cfg(all(feature = "mobile-preview", feature = "avf", target_os = "ios"))] +impl Camera { + pub fn session_handle(&self) -> Result { + self.driver.session_handle() + } +} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 0a1ddf0..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,409 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::CameraError; -use clientele::StandardOptions; - -#[derive(Clone, Debug)] -pub struct DeviceInfo { - pub id: String, - pub name: String, - pub is_usb: bool, -} - -pub fn list_video_devices(flags: &StandardOptions) -> Result, CameraError> { - #[cfg(target_os = "macos")] - { - return macos_list_video_devices(flags); - } - #[cfg(target_os = "linux")] - { - return linux_list_video_devices(flags); - } - #[cfg(target_os = "windows")] - { - return windows_list_video_devices(flags); - } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - let _ = flags; - return Ok(Vec::new()); - } -} - -pub fn auto_select_device( - flags: &StandardOptions, - preferred: Option, -) -> Result, CameraError> { - if let Some(p) = preferred { - return Ok(Some(normalize_device_id(&p))); - } - - let devices = list_video_devices(flags)?; - if devices.is_empty() { - return Ok(None); - } - - #[cfg(target_os = "macos")] - { - if let Some(id) = macos_prefer_usb(&devices) { - return Ok(Some(id)); - } - } - - if let Some(d) = devices.iter().find(|d| d.is_usb) { - return Ok(Some(d.id.clone())); - } - - Ok(Some(devices[0].id.clone())) -} - -pub fn normalize_device_id(raw: &str) -> String { - let s = raw.trim(); - - if s.starts_with("avf:") || s.starts_with("file:") || s.starts_with("dshow:") { - return s.to_string(); - } - - #[cfg(target_os = "macos")] - { - if s.chars().all(|c| c.is_ascii_digit()) { - return format!("avf:{s}"); - } - } - - #[cfg(target_os = "linux")] - { - if s.starts_with("/dev/video") { - return format!("file:{s}"); - } - } - - #[cfg(target_os = "windows")] - { - if s.starts_with("video=") { - return format!("dshow:{s}"); - } - if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 { - let inner = &s[1..s.len() - 1]; - if !inner.is_empty() { - return format!("dshow:video={inner}"); - } - } - } - - s.to_string() -} - -fn contains_case_insensitive(haystack: &str, needle: &str) -> bool { - haystack.to_lowercase().contains(&needle.to_lowercase()) -} - -#[cfg(target_os = "macos")] -fn macos_list_video_devices(flags: &StandardOptions) -> Result, CameraError> { - use std::process::Command; - - if flags.debug || flags.verbose >= 2 { - eprintln!("INFO: listing macOS AVFoundation devices via ffmpeg"); - } - - let out = Command::new("ffmpeg") - .args([ - "-hide_banner", - "-f", - "avfoundation", - "-list_devices", - "true", - "-i", - "", - ]) - .output() - .map_err(|e| CameraError::driver("running ffmpeg -list_devices", e))?; - - let stderr = String::from_utf8_lossy(&out.stderr); - let avf = parse_avfoundation_video_devices(&stderr).unwrap_or_default(); - - let usb_names = macos_usb_product_names().unwrap_or_default(); - - let mut devs = Vec::new(); - for d in avf { - let is_usb = usb_names - .iter() - .any(|u| contains_case_insensitive(&d.name, u)); - devs.push(DeviceInfo { - id: format!("avf:{}", d.index), - name: d.name, - is_usb, - }); - } - - Ok(devs) -} - -#[cfg(target_os = "macos")] -fn macos_prefer_usb(devices: &[DeviceInfo]) -> Option { - let usb_names = macos_usb_product_names().unwrap_or_default(); - if usb_names.is_empty() { - return None; - } - for d in devices { - if usb_names - .iter() - .any(|u| contains_case_insensitive(&d.name, u)) - { - return Some(d.id.clone()); - } - } - None -} - -#[cfg(target_os = "macos")] -#[derive(Clone, Debug)] -struct AvfVideoDevice { - index: u32, - name: String, -} - -#[cfg(target_os = "macos")] -fn parse_avfoundation_video_devices(s: &str) -> Option> { - let mut devices = Vec::new(); - let mut in_video = false; - - for line in s.lines() { - if line.contains("AVFoundation video devices:") { - in_video = true; - continue; - } - if line.contains("AVFoundation audio devices:") { - break; - } - if !in_video { - continue; - } - - let Some(pos) = line.find("] [") else { - continue; - }; - let tail = line[pos + 2..].trim(); - - if !tail.starts_with('[') { - continue; - } - let Some(end_bracket) = tail.find(']') else { - continue; - }; - - let idx_str = &tail[1..end_bracket]; - let idx: u32 = match idx_str.trim().parse() { - Ok(v) => v, - Err(_) => continue, - }; - - let name = tail[end_bracket + 1..].trim(); - if name.is_empty() { - continue; - } - - devices.push(AvfVideoDevice { - index: idx, - name: name.to_string(), - }); - } - - if devices.is_empty() { - None - } else { - Some(devices) - } -} - -#[cfg(target_os = "macos")] -fn macos_usb_product_names() -> Option> { - let out = std::process::Command::new("ioreg") - .args(["-p", "IOUSB", "-l"]) - .output() - .ok()?; - - if !out.status.success() { - return None; - } - - let s = String::from_utf8_lossy(&out.stdout); - let mut names = Vec::new(); - - for line in s.lines() { - let line = line.trim(); - if let Some(v) = extract_quoted_value(line, "\"USB Product Name\"") { - names.push(v); - } else if let Some(v) = extract_quoted_value(line, "\"kUSBProductString\"") { - names.push(v); - } - } - - names.sort(); - names.dedup(); - - if names.is_empty() { None } else { Some(names) } -} - -#[cfg(target_os = "macos")] -fn extract_quoted_value(line: &str, key: &str) -> Option { - if !line.contains(key) { - return None; - } - let eq = line.find('=')?; - let rhs = line[eq + 1..].trim(); - let first = rhs.find('"')?; - let rest = &rhs[first + 1..]; - let last = rest.find('"')?; - Some(rest[..last].to_string()) -} - -#[cfg(target_os = "linux")] -fn linux_list_video_devices(flags: &StandardOptions) -> Result, CameraError> { - use std::{fs, path::Path}; - - let base = Path::new("/sys/class/video4linux"); - let mut idxs: Vec = Vec::new(); - - let rd = match fs::read_dir(base) { - Ok(v) => v, - Err(_) => return Ok(Vec::new()), - }; - - for e in rd.flatten() { - let name = match e.file_name().to_str().map(|s| s.to_string()) { - Some(v) => v, - None => continue, - }; - if !name.starts_with("video") { - continue; - } - let idx: u32 = match name[5..].parse() { - Ok(v) => v, - Err(_) => continue, - }; - idxs.push(idx); - } - - idxs.sort_unstable(); - - if flags.debug || flags.verbose >= 2 { - eprintln!("INFO: found video nodes: {idxs:?}"); - } - - let mut out = Vec::new(); - for idx in idxs { - let devnode = format!("/dev/video{idx}"); - if !Path::new(&devnode).exists() { - continue; - } - - let sys = base.join(format!("video{idx}")); - let name = fs::read_to_string(sys.join("name")) - .ok() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| devnode.clone()); - - let is_usb = linux_is_usb(&sys); - - out.push(DeviceInfo { - id: format!("file:{devnode}"), - name, - is_usb, - }); - } - - Ok(out) -} - -#[cfg(target_os = "linux")] -fn linux_is_usb(sys_video: &std::path::Path) -> bool { - use std::fs; - let dev = sys_video.join("device"); - let link = fs::read_link(&dev).ok(); - if let Some(p) = link { - let s = p.to_string_lossy().to_lowercase(); - if s.contains("usb") { - return true; - } - } - - let uevent = fs::read_to_string(dev.join("uevent")) - .ok() - .unwrap_or_default(); - let u = uevent.to_lowercase(); - u.contains("usb") -} - -#[cfg(target_os = "windows")] -fn windows_list_video_devices(flags: &StandardOptions) -> Result, CameraError> { - use std::process::Command; - - if flags.debug || flags.verbose >= 2 { - eprintln!("INFO: listing Windows DirectShow devices via ffmpeg"); - } - - let out = Command::new("ffmpeg") - .args([ - "-hide_banner", - "-f", - "dshow", - "-list_devices", - "true", - "-i", - "dummy", - ]) - .output() - .map_err(|e| CameraError::driver("running ffmpeg -list_devices", e))?; - - let stderr = String::from_utf8_lossy(&out.stderr); - Ok(parse_dshow_video_devices(&stderr)) -} - -#[cfg(target_os = "windows")] -fn parse_dshow_video_devices(s: &str) -> Vec { - let mut out = Vec::new(); - let mut in_video = false; - - for line in s.lines() { - if line.contains("DirectShow video devices") { - in_video = true; - continue; - } - if in_video && line.contains("DirectShow audio devices") { - break; - } - if !in_video { - continue; - } - - if let Some(name) = extract_dshow_quoted_name(line) { - let n = name.to_lowercase(); - let is_usb = n.contains("usb") || n.contains("webcam") || n.contains("capture"); - out.push(DeviceInfo { - id: format!("dshow:video={}", name), - name, - is_usb, - }); - } - } - - out -} - -#[cfg(target_os = "windows")] -fn extract_dshow_quoted_name(line: &str) -> Option { - let l = line.trim(); - if !l.starts_with('"') { - return None; - } - let rest = &l[1..]; - let end = rest.find('"')?; - let name = &rest[..end]; - if name.is_empty() { - None - } else { - Some(name.to_string()) - } -} diff --git a/src/converter.rs b/src/converter.rs new file mode 100644 index 0000000..3c5bd81 --- /dev/null +++ b/src/converter.rs @@ -0,0 +1,301 @@ +// This is free and unencumbered software released into the public domain. + +use std::sync::Arc; + +use crate::{Frame, FrameRef, PixelFormat, RawFormat, RawFrame, RawFrameRef}; + +#[inline] +fn clamp_u8(v: i32) -> u8 { + if v <= 0 { + 0 + } else if v >= 255 { + 255 + } else { + v as u8 + } +} + +#[inline] +fn checked_min_row_bytes(width: u32, bytes_per_pixel: u32) -> usize { + (width as usize).saturating_mul(bytes_per_pixel as usize) +} + +#[inline] +fn get_packed_plane(raw: &RawFrame) -> Option<(&[u8], u32, u32)> { + let p = raw.planes.get(0)?; + Some((p.data.as_ref(), p.row_stride, p.pixel_stride)) +} + +pub fn convert_to_packed_rgb8(raw_ref: RawFrameRef) -> Option { + let raw = raw_ref.as_ref(); + + match raw.format { + RawFormat::PackedRgb8 => Some(raw_ref), + + RawFormat::PackedBgra8 => { + let (src, row_stride, pixel_stride) = get_packed_plane(raw)?; + let rgb = bgra_to_rgb(src, raw.width, raw.height, row_stride, pixel_stride)?; + let out = RawFrame::new_rgb8( + raw.width, + raw.height, + rgb, + raw.width.saturating_mul(3), + raw.timestamp_ns, + ); + Some(Arc::new(out)) + }, + + RawFormat::AndroidYuv420888 => { + let rgb = yuv420888_to_rgb8(raw_ref.clone())?; + let out = RawFrame::new_rgb8( + raw.width, + raw.height, + rgb, + raw.width.saturating_mul(3), + raw.timestamp_ns, + ); + Some(Arc::new(out)) + }, + } +} + +pub fn convert_to_packed_bgra8(raw_ref: RawFrameRef) -> Option { + let raw = raw_ref.as_ref(); + + match raw.format { + RawFormat::PackedBgra8 => Some(raw_ref), + + RawFormat::PackedRgb8 => { + let (src, row_stride, pixel_stride) = get_packed_plane(raw)?; + let bgra = rgb_to_bgra(src, raw.width, raw.height, row_stride, pixel_stride)?; + let out = RawFrame::new_bgra8( + raw.width, + raw.height, + bgra, + raw.width.saturating_mul(4), + raw.timestamp_ns, + ); + Some(Arc::new(out)) + }, + + RawFormat::AndroidYuv420888 => { + let rgb_ref = convert_to_packed_rgb8(raw_ref)?; + let rgb = rgb_ref.as_ref(); + let (src, row_stride, pixel_stride) = get_packed_plane(rgb)?; + let bgra = rgb_to_bgra(src, rgb.width, rgb.height, row_stride, pixel_stride)?; + let out = RawFrame::new_bgra8( + rgb.width, + rgb.height, + bgra, + rgb.width.saturating_mul(4), + rgb.timestamp_ns, + ); + Some(Arc::new(out)) + }, + } +} + +pub fn convert_raw_to_frame(raw_ref: RawFrameRef, output: PixelFormat) -> Option { + match output { + PixelFormat::Rgb8 => { + let packed_ref = convert_to_packed_rgb8(raw_ref)?; + let raw = packed_ref.as_ref(); + + if raw.format != RawFormat::PackedRgb8 || raw.planes.is_empty() { + return None; + } + let p0 = raw.planes.get(0)?; + let min_row = checked_min_row_bytes(raw.width, 3); + if (p0.row_stride as usize) < min_row || p0.pixel_stride != 3 { + return None; + } + + Some(Arc::new(Frame::new( + raw.width, + raw.height, + p0.row_stride.max(raw.width.saturating_mul(3)), + PixelFormat::Rgb8, + p0.data.clone(), + raw.timestamp_ns, + ))) + }, + + PixelFormat::Bgra8 => { + let packed_ref = convert_to_packed_bgra8(raw_ref)?; + let raw = packed_ref.as_ref(); + + if raw.format != RawFormat::PackedBgra8 || raw.planes.is_empty() { + return None; + } + let p0 = raw.planes.get(0)?; + let min_row = checked_min_row_bytes(raw.width, 4); + if (p0.row_stride as usize) < min_row || p0.pixel_stride != 4 { + return None; + } + + Some(Arc::new(Frame { + width: raw.width, + height: raw.height, + stride: p0.row_stride.max(raw.width.saturating_mul(4)), + pixel_format: PixelFormat::Bgra8, + data: p0.data.clone(), + timestamp_ns: raw.timestamp_ns, + })) + }, + } +} + +fn bgra_to_rgb(src: &[u8], w: u32, h: u32, row_stride: u32, pixel_stride: u32) -> Option> { + if w == 0 || h == 0 || pixel_stride == 0 || row_stride == 0 { + return None; + } + + let w_us = w as usize; + let h_us = h as usize; + let rs = row_stride as usize; + let ps = pixel_stride as usize; + + if ps < 4 { + return None; + } + + let mut out = vec![0u8; w_us.saturating_mul(h_us).saturating_mul(3)]; + + for y in 0..h_us { + let row_off = y.checked_mul(rs)?; + for x in 0..w_us { + let i = row_off.checked_add(x.checked_mul(ps)?)?; + if i + 3 >= src.len() { + return None; + } + + let b = src[i + 0]; + let g = src[i + 1]; + let r = src[i + 2]; + + let o = (y * w_us + x) * 3; + out[o + 0] = r; + out[o + 1] = g; + out[o + 2] = b; + } + } + + Some(out) +} + +fn rgb_to_bgra(src: &[u8], w: u32, h: u32, row_stride: u32, pixel_stride: u32) -> Option> { + if w == 0 || h == 0 || pixel_stride == 0 || row_stride == 0 { + return None; + } + + let w_us = w as usize; + let h_us = h as usize; + let rs = row_stride as usize; + let ps = pixel_stride as usize; + + if ps < 3 { + return None; + } + + let mut out = vec![0u8; w_us.saturating_mul(h_us).saturating_mul(4)]; + + for y in 0..h_us { + let row_off = y.checked_mul(rs)?; + for x in 0..w_us { + let i = row_off.checked_add(x.checked_mul(ps)?)?; + if i + 2 >= src.len() { + return None; + } + + let r = src[i + 0]; + let g = src[i + 1]; + let b = src[i + 2]; + + let o = (y * w_us + x) * 4; + out[o + 0] = b; + out[o + 1] = g; + out[o + 2] = r; + out[o + 3] = 255; + } + } + + Some(out) +} + +fn yuv420888_to_rgb8(raw_ref: RawFrameRef) -> Option> { + let raw = raw_ref.as_ref(); + + let w = raw.width as usize; + let h = raw.height as usize; + if w == 0 || h == 0 { + return None; + } + if raw.planes.len() < 3 { + return None; + } + + let y_plane = &raw.planes[0]; + let u_plane = &raw.planes[1]; + let v_plane = &raw.planes[2]; + + let y = y_plane.data.as_ref(); + let u = u_plane.data.as_ref(); + let v = v_plane.data.as_ref(); + + let y_rs = y_plane.row_stride as usize; + let y_ps = y_plane.pixel_stride as usize; + + let u_rs = u_plane.row_stride as usize; + let u_ps = u_plane.pixel_stride as usize; + + let v_rs = v_plane.row_stride as usize; + let v_ps = v_plane.pixel_stride as usize; + + if y_rs == 0 || y_ps == 0 || u_rs == 0 || u_ps == 0 || v_rs == 0 || v_ps == 0 { + return None; + } + + let y_min_last = (h - 1) + .checked_mul(y_rs)? + .checked_add((w - 1).checked_mul(y_ps)?)?; + if y_min_last >= y.len() { + return None; + } + + let mut out = vec![0u8; w.saturating_mul(h).saturating_mul(3)]; + + for yy in 0..h { + let y_row = yy.checked_mul(y_rs)?; + let u_row = (yy / 2).checked_mul(u_rs)?; + let v_row = (yy / 2).checked_mul(v_rs)?; + + for xx in 0..w { + let y_idx = y_row.checked_add(xx.checked_mul(y_ps)?)?; + let u_idx = u_row.checked_add((xx / 2).checked_mul(u_ps)?)?; + let v_idx = v_row.checked_add((xx / 2).checked_mul(v_ps)?)?; + + if y_idx >= y.len() || u_idx >= u.len() || v_idx >= v.len() { + return None; + } + + let yv = y[y_idx] as i32; + let uv = u[u_idx] as i32; + let vv = v[v_idx] as i32; + + let c = yv - 16; + let d = uv - 128; + let e = vv - 128; + + let r = (298 * c + 409 * e + 128) >> 8; + let g = (298 * c - 100 * d - 208 * e + 128) >> 8; + let b = (298 * c + 516 * d + 128) >> 8; + + let o = (yy * w + xx) * 3; + out[o + 0] = clamp_u8(r); + out[o + 1] = clamp_u8(g); + out[o + 2] = clamp_u8(b); + } + } + + Some(out) +} diff --git a/src/drivers/android/devices.rs b/src/drivers/android/devices.rs new file mode 100644 index 0000000..9814902 --- /dev/null +++ b/src/drivers/android/devices.rs @@ -0,0 +1,84 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraError, DeviceInfo, DeviceKind}; + +use core::ffi::{c_char, c_int}; + +use super::ndk::CameraManager; + +fn cstr_to_string(ptr: *const c_char) -> String { + if ptr.is_null() { + return String::new(); + } + let cstr = unsafe { core::ffi::CStr::from_ptr(ptr) }; + cstr.to_string_lossy().to_string() +} + +#[inline] +fn tag_lens_facing() -> u32 { + ndk_sys::acamera_metadata_tag::ACAMERA_LENS_FACING.0 as u32 +} + +fn classify_kind(mgr: &CameraManager, camera_id: *const c_char) -> DeviceKind { + use ndk_sys as ndk; + + unsafe { + let mut meta: *mut ndk::ACameraMetadata = core::ptr::null_mut(); + let status = mgr.get_characteristics(camera_id, &mut meta); + + if status != ndk::camera_status_t::ACAMERA_OK || meta.is_null() { + return DeviceKind::Unknown; + } + + let mut entry: ndk::ACameraMetadata_const_entry = core::mem::zeroed(); + let st = ndk::ACameraMetadata_getConstEntry( + meta as *const ndk::ACameraMetadata, + tag_lens_facing(), + &mut entry, + ); + + ndk::ACameraMetadata_free(meta); + + if st != ndk::camera_status_t::ACAMERA_OK { + return DeviceKind::Unknown; + } + + if entry.data.u8_.is_null() { + return DeviceKind::Unknown; + } + + let facing_u8: u8 = *entry.data.u8_; + match facing_u8 as c_int { + 0 => DeviceKind::Front, + 1 => DeviceKind::Back, + 2 => DeviceKind::External, + _ => DeviceKind::Unknown, + } + } +} + +pub fn list_video_devices() -> Result, CameraError> { + let mgr = CameraManager::new() + .map_err(|st| CameraError::driver("android: ACameraManager_create", st))?; + + let id_list = mgr + .list_camera_ids() + .map_err(|st| CameraError::driver("android: ACameraManager_getCameraIdList", st))?; + + let n = id_list.len(); + let mut out = Vec::with_capacity(n); + + for i in 0..n { + let id_ptr = id_list.id_ptr(i); + if id_ptr.is_null() { + continue; + } + + let id = cstr_to_string(id_ptr); + let kind = classify_kind(&mgr, id_ptr); + + out.push(DeviceInfo::new(id.clone(), id, kind)); + } + + Ok(out) +} diff --git a/src/drivers/android/driver/callbacks.rs b/src/drivers/android/driver/callbacks.rs new file mode 100644 index 0000000..744b94e --- /dev/null +++ b/src/drivers/android/driver/callbacks.rs @@ -0,0 +1,84 @@ +// This is free and unencumbered software released into the public domain. + +use core::ffi::c_void; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +use crossbeam_channel as ch; +use ndk_sys as ndk; + +use crate::RawFrameRef; + +pub struct State { + pub last_device_error: AtomicI32, + pub disconnected: AtomicBool, + pub raw_tx: ch::Sender, +} + +impl State { + pub fn new(raw_tx: ch::Sender) -> Self { + Self { + last_device_error: AtomicI32::new(0), + disconnected: AtomicBool::new(false), + raw_tx, + } + } +} + +#[inline] +pub fn state_ptr(st: &Arc) -> *mut c_void { + Arc::as_ptr(st) as *mut c_void +} + +extern "C" fn on_disconnected(ctx: *mut c_void, _device: *mut ndk::ACameraDevice) { + if ctx.is_null() { + return; + } + let st = unsafe { &*(ctx as *const State) }; + st.disconnected.store(true, Ordering::Release); +} + +extern "C" fn on_error(ctx: *mut c_void, _device: *mut ndk::ACameraDevice, error_code: i32) { + if ctx.is_null() { + return; + } + let st = unsafe { &*(ctx as *const State) }; + st.last_device_error.store(error_code, Ordering::Release); +} + +pub fn build_device_callbacks(state_ptr: *mut c_void) -> Box { + Box::new(ndk::ACameraDevice_StateCallbacks { + context: state_ptr, + onDisconnected: Some(on_disconnected), + onError: Some(on_error), + }) +} + +extern "C" fn on_session_ready(ctx: *mut c_void, _session: *mut ndk::ACameraCaptureSession) { + if ctx.is_null() { + return; + } + let st = unsafe { &*(ctx as *const State) }; + let _ = st.last_device_error.load(Ordering::Acquire); +} + +extern "C" fn on_session_active(ctx: *mut c_void, _session: *mut ndk::ACameraCaptureSession) { + if ctx.is_null() { + return; + } + let st = unsafe { &*(ctx as *const State) }; + let _ = st.last_device_error.load(Ordering::Acquire); +} + +extern "C" fn on_session_closed(_ctx: *mut c_void, _session: *mut ndk::ACameraCaptureSession) {} + +pub fn build_session_callbacks( + state_ptr: *mut c_void, +) -> Box { + Box::new(ndk::ACameraCaptureSession_stateCallbacks { + context: state_ptr, + onClosed: Some(on_session_closed), + onReady: Some(on_session_ready), + onActive: Some(on_session_active), + }) +} diff --git a/src/drivers/android/driver/image_stream.rs b/src/drivers/android/driver/image_stream.rs new file mode 100644 index 0000000..1a718e8 --- /dev/null +++ b/src/drivers/android/driver/image_stream.rs @@ -0,0 +1,104 @@ +// This is free and unencumbered software released into the public domain. + +use core::ffi::c_void; + +use bytes::Bytes; +use ndk_sys as ndk; + +use crate::{RawFormat, RawFrame, RawFrameRef, RawPlane}; + +use super::callbacks::State; +use crate::drivers::android::ndk::{ImageReader, NativeWindow, acquire_latest_image_from_raw}; + +use std::sync::Arc; + +pub struct ImageStream { + reader: ImageReader, + window: NativeWindow, +} + +impl ImageStream { + pub fn new( + dimensions: (u32, u32), + format: i32, + max_images: i32, + state_ptr: *mut c_void, + ) -> Result { + let mut reader = ImageReader::new(dimensions, format, max_images)?; + + let mut window = reader.get_window()?; + window.acquire(); + + extern "C" fn on_image_available(ctx: *mut c_void, reader: *mut ndk::AImageReader) { + if ctx.is_null() || reader.is_null() { + return; + } + let st = unsafe { &*(ctx as *const State) }; + + if st.raw_tx.is_full() { + let _ = unsafe { acquire_latest_image_from_raw(reader) }; + return; + } + + let img = match unsafe { acquire_latest_image_from_raw(reader) } { + Ok(v) => v, + Err(_) => return, + }; + + let width = match img.get_width() { + Ok(v) => v, + Err(_) => return, + }; + let height = match img.get_height() { + Ok(v) => v, + Err(_) => return, + }; + + let ts_ns = img.get_timestamp().ok().map(|v| v.max(0) as u64); + + let mut planes = Vec::with_capacity(3); + + for plane_idx in 0..3 { + let data = match img.copy_plane_data(plane_idx) { + Ok(v) => v, + Err(_) => return, + }; + + let row_stride = + img.get_plane_row_stride(plane_idx).ok().unwrap_or(0).max(0) as u32; + let pixel_stride = img + .get_plane_pixel_stride(plane_idx) + .ok() + .unwrap_or(1) + .max(1) as u32; + + planes.push(RawPlane::new(Bytes::from(data), row_stride, pixel_stride)); + } + + let raw = RawFrame::new(width, height, RawFormat::AndroidYuv420888, planes, ts_ns); + let frame_ref: RawFrameRef = Arc::new(raw); + + let _ = st.raw_tx.try_send(frame_ref); + } + + reader.set_image_listener(state_ptr, on_image_available)?; + + Ok(Self { reader, window }) + } + + #[inline] + pub fn window_ptr(&self) -> *mut ndk::ANativeWindow { + self.window.as_ptr() + } + + pub fn close(&mut self) { + self.reader.close(); + self.window = NativeWindow::default(); + } +} + +impl Drop for ImageStream { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/driver/mod.rs b/src/drivers/android/driver/mod.rs new file mode 100644 index 0000000..75b1251 --- /dev/null +++ b/src/drivers/android/driver/mod.rs @@ -0,0 +1,121 @@ +// This is free and unencumbered software released into the public domain. + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crossbeam_channel as ch; + +use crate::drivers::CameraDriver; +use crate::{CameraBackend, CameraError, RawFrameRef}; + +use crate::drivers::DriverConfig; + +use super::ndk::{CameraDevice, CameraManager, NativeWindow}; + +mod callbacks; +mod image_stream; +mod open; +mod session_graph; +mod sizes; + +use callbacks::State; +use image_stream::ImageStream; +use session_graph::SessionGraph; + +pub struct AndroidDriver { + _cfg: DriverConfig, + + _raw_tx: ch::Sender, + raw_rx: ch::Receiver, + + _mgr: CameraManager, + _dev: CameraDevice, + + preview_window: NativeWindow, + image_stream: ImageStream, + graph: SessionGraph, + running: AtomicBool, + closed: AtomicBool, + _state: Arc, + + picked_w: u32, + picked_h: u32, + rotation_deg: i32, +} + +pub fn try_open(cfg: &DriverConfig) -> Result, CameraError> { + open::open(cfg).map(|d| Box::new(d) as Box) +} + +impl AndroidDriver { + fn teardown(&mut self) { + if self.closed.swap(true, Ordering::AcqRel) { + return; + } + + let _ = self.graph.stop_repeating(); + self.graph.close(); + self.image_stream.close(); + self.preview_window = NativeWindow::default(); + } +} + +impl Drop for AndroidDriver { + fn drop(&mut self) { + self.teardown(); + } +} + +impl CameraDriver for AndroidDriver { + fn backend(&self) -> CameraBackend { + CameraBackend::Android + } + + fn start(&mut self) -> Result<(), CameraError> { + if self.closed.load(Ordering::Acquire) { + return Err(CameraError::Closed); + } + if self.running.swap(true, Ordering::AcqRel) { + return Ok(()); + } + + self.graph.start_repeating().map_err(|e| { + self.running.store(false, Ordering::Release); + e + })?; + + Ok(()) + } + + fn stop(&mut self) -> Result<(), CameraError> { + if self.closed.load(Ordering::Acquire) { + return Ok(()); + } + if !self.running.swap(false, Ordering::AcqRel) { + return Ok(()); + } + + self.graph.stop_repeating()?; + Ok(()) + } + + fn close(&mut self) -> Result<(), CameraError> { + let _ = self.stop(); + self.teardown(); + Ok(()) + } + + fn read_frames(&mut self) -> Result, CameraError> { + if self.closed.load(Ordering::Acquire) { + return Err(CameraError::Closed); + } + Ok(self.raw_rx.clone()) + } + + fn preview_info(&self) -> Option<(u32, u32, i32)> { + if self.picked_w == 0 || self.picked_h == 0 { + return None; + } + Some((self.picked_w, self.picked_h, self.rotation_deg)) + } +} diff --git a/src/drivers/android/driver/open.rs b/src/drivers/android/driver/open.rs new file mode 100644 index 0000000..e9d79c5 --- /dev/null +++ b/src/drivers/android/driver/open.rs @@ -0,0 +1,120 @@ +// This is free and unencumbered software released into the public domain. + +use std::ffi::{CString, c_char, c_void}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use crossbeam_channel as ch; +use ndk_sys as ndk; + +use crate::drivers::DriverConfig; +use crate::{CameraError, RawFrameRef}; + +use super::callbacks; +use super::image_stream::ImageStream; +use super::session_graph::SessionGraph; +use super::sizes; + +use crate::drivers::android::ndk::{CameraManager, NativeWindow}; + +use super::AndroidDriver; + +pub fn open(cfg: &DriverConfig) -> Result { + let cfg = cfg.clone(); + cfg.validate()?; + + let id = cfg.device.id().trim(); + if id.is_empty() { + return Err(CameraError::invalid_config("Android camera id is empty")); + } + + let cap = cfg.buffer_raw.max(1).min(8); + let (raw_tx, raw_rx) = ch::bounded::(cap); + + let preview_ptr: *mut ndk::ANativeWindow = + cfg.android_preview.as_ptr().cast::(); + + if preview_ptr.is_null() { + return Err(CameraError::invalid_config( + "android_preview must be a non-null ANativeWindow pointer", + )); + } + + let mut preview_window = NativeWindow::from_ptr_borrowed(preview_ptr); + preview_window.acquire(); + let pw = preview_window.width(); + let ph = preview_window.height(); + + let mgr = CameraManager::new() + .map_err(|st| CameraError::driver("android: ACameraManager_create", st))?; + + let camera_id = + CString::new(id).map_err(|_| CameraError::invalid_config("device id contains NUL byte"))?; + + const AIMAGE_FORMAT_YUV_420_888: i32 = 35; + + let desired_w: i32 = if pw > 0 { pw } else { cfg.width as i32 }; + let desired_h: i32 = if ph > 0 { ph } else { cfg.height as i32 }; + + let supported = sizes::list_supported_output_sizes( + &mgr, + camera_id.as_ptr() as *const c_char, + AIMAGE_FORMAT_YUV_420_888, + ); + + let (best_w, best_h) = + sizes::pick_best_size(desired_w, desired_h, &supported).unwrap_or((1280, 720)); + + let preview_fmt = preview_window.format(); + let _ = preview_window.set_buffers_geometry(best_w, best_h, preview_fmt); + + let state = Arc::new(callbacks::State::new(raw_tx.clone())); + let state_ptr = callbacks::state_ptr(&state) as *mut c_void; + + let device_callbacks = callbacks::build_device_callbacks(state_ptr); + + let dev = mgr + .open_camera(camera_id.as_ptr() as *const c_char, device_callbacks) + .map_err(|st| CameraError::driver("android: ACameraManager_openCamera", st))?; + + let max_images: i32 = (cfg.buffer_raw.max(2).min(4)) as i32; + + let image_stream = ImageStream::new( + (best_w as u32, best_h as u32), + AIMAGE_FORMAT_YUV_420_888, + max_images, + state_ptr, + ) + .map_err(|st| CameraError::driver("android: ImageStream::new", st))?; + + let session_callbacks = callbacks::build_session_callbacks(state_ptr); + + let graph = SessionGraph::new( + &dev, + &mut preview_window, + image_stream.window_ptr(), + session_callbacks, + )?; + + let rotation_deg: i32 = 90; + + Ok(AndroidDriver { + _cfg: cfg, + _raw_tx: raw_tx, + raw_rx, + + _mgr: mgr, + _dev: dev, + + preview_window, + image_stream, + graph, + running: AtomicBool::new(false), + closed: AtomicBool::new(false), + _state: state, + + picked_w: best_w.max(0) as u32, + picked_h: best_h.max(0) as u32, + rotation_deg, + }) +} diff --git a/src/drivers/android/driver/session_graph.rs b/src/drivers/android/driver/session_graph.rs new file mode 100644 index 0000000..5d2ce6e --- /dev/null +++ b/src/drivers/android/driver/session_graph.rs @@ -0,0 +1,129 @@ +// This is free and unencumbered software released into the public domain. + +use ndk_sys as ndk; + +use crate::CameraError; + +use crate::drivers::android::ndk::{ + CameraCaptureSession, CameraDevice, CameraOutputTarget, CaptureRequest, CaptureSessionOutput, + CaptureSessionOutputContainer, NativeWindow, +}; + +pub struct SessionGraph { + session: CameraCaptureSession, + request: CaptureRequest, + output_container: CaptureSessionOutputContainer, + preview_output: CaptureSessionOutput, + capture_output: CaptureSessionOutput, + preview_target: CameraOutputTarget, + capture_target: CameraOutputTarget, +} + +impl SessionGraph { + pub fn new( + dev: &CameraDevice, + preview_window: &mut NativeWindow, + capture_window_ptr: *mut ndk::ANativeWindow, + session_callbacks: Box, + ) -> Result { + if preview_window.as_ptr().is_null() { + return Err(CameraError::invalid_config("preview window is null")); + } + if capture_window_ptr.is_null() { + return Err(CameraError::invalid_config( + "capture (ImageReader) window is null", + )); + } + + let request = + CaptureRequest::new(dev, ndk::ACameraDevice_request_template::TEMPLATE_RECORD) + .map_err(|st| { + CameraError::driver("android: ACameraDevice_createCaptureRequest", st) + })?; + + let output_container = CaptureSessionOutputContainer::new().map_err(|st| { + CameraError::driver("android: ACaptureSessionOutputContainer_create", st) + })?; + + let preview_output = CaptureSessionOutput::new(preview_window.as_ptr()).map_err(|st| { + CameraError::driver("android: ACaptureSessionOutput_create(preview)", st) + })?; + + let capture_output = CaptureSessionOutput::new(capture_window_ptr).map_err(|st| { + CameraError::driver("android: ACaptureSessionOutput_create(capture)", st) + })?; + + output_container.add(&preview_output).map_err(|st| { + CameraError::driver("android: ACaptureSessionOutputContainer_add(preview)", st) + })?; + + output_container.add(&capture_output).map_err(|st| { + CameraError::driver("android: ACaptureSessionOutputContainer_add(capture)", st) + })?; + + let preview_target = CameraOutputTarget::new(preview_window.as_ptr()).map_err(|st| { + CameraError::driver("android: ACameraOutputTarget_create(preview)", st) + })?; + + let capture_target = CameraOutputTarget::new(capture_window_ptr).map_err(|st| { + CameraError::driver("android: ACameraOutputTarget_create(capture)", st) + })?; + + request + .add_target(&preview_target) + .map_err(|st| CameraError::driver("android: ACaptureRequest_addTarget(preview)", st))?; + + request + .add_target(&capture_target) + .map_err(|st| CameraError::driver("android: ACaptureRequest_addTarget(capture)", st))?; + + let session = CameraCaptureSession::open(dev, output_container.as_ptr(), session_callbacks) + .map_err(|st| CameraError::driver("android: ACameraDevice_createCaptureSession", st))?; + + Ok(Self { + session, + request, + output_container, + preview_output, + capture_output, + preview_target, + capture_target, + }) + } + + pub fn start_repeating(&self) -> Result<(), CameraError> { + let mut seq_id: i32 = 0; + + self.session + .set_repeating_request(self.request.as_ptr(), Some(&mut seq_id)) + .map_err(|st| { + CameraError::driver("android: ACameraCaptureSession_setRepeatingRequest", st) + })?; + + Ok(()) + } + + pub fn stop_repeating(&self) -> Result<(), CameraError> { + self.session.stop_repeating().map_err(|st| { + CameraError::driver("android: ACameraCaptureSession_stopRepeating", st) + })?; + + Ok(()) + } + + pub fn close(&mut self) { + self.session.close(); + self.request.close(); + self.capture_target.close(); + self.preview_target.close(); + self.capture_output.close(); + self.preview_output.close(); + self.output_container.close(); + } +} + +impl Drop for SessionGraph { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/driver/sizes.rs b/src/drivers/android/driver/sizes.rs new file mode 100644 index 0000000..bb4bff5 --- /dev/null +++ b/src/drivers/android/driver/sizes.rs @@ -0,0 +1,114 @@ +// This is free and unencumbered software released into the public domain. + +use core::ffi::c_char; +use core::ptr::null_mut; + +use ndk_sys as ndk; + +use crate::drivers::android::ndk::CameraManager; + +#[inline] +fn tag_available_stream_configurations() -> u32 { + ndk::acamera_metadata_tag::ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS.0 as u32 +} + +pub(super) fn list_supported_output_sizes( + mgr: &CameraManager, + camera_id: *const c_char, + desired_format: i32, +) -> Vec<(i32, i32)> { + unsafe { + let mut meta: *mut ndk::ACameraMetadata = null_mut(); + let st = mgr.get_characteristics(camera_id, &mut meta); + + if st != ndk::camera_status_t::ACAMERA_OK || meta.is_null() { + return Vec::new(); + } + + let mut entry: ndk::ACameraMetadata_const_entry = core::mem::zeroed(); + let st_e = ndk::ACameraMetadata_getConstEntry( + meta as *const ndk::ACameraMetadata, + tag_available_stream_configurations(), + &mut entry, + ); + + if st_e != ndk::camera_status_t::ACAMERA_OK { + ndk::ACameraMetadata_free(meta); + return Vec::new(); + } + + let count = entry.count as usize; + let tuples = count / 4; + let mut out = Vec::new(); + + if entry.data.i32_.is_null() || tuples == 0 { + ndk::ACameraMetadata_free(meta); + return Vec::new(); + } + + let base = entry.data.i32_; + + for i in 0..tuples { + let off = i * 4; + let fmt = *base.add(off + 0); + let w = *base.add(off + 1); + let h = *base.add(off + 2); + let input = *base.add(off + 3); + + if input != 0 { + continue; + } + if fmt != desired_format { + continue; + } + if w > 0 && h > 0 { + out.push((w, h)); + } + } + + ndk::ACameraMetadata_free(meta); + out.sort_unstable(); + out.dedup(); + out + } +} + +pub(super) fn pick_best_size( + desired_w: i32, + desired_h: i32, + candidates: &[(i32, i32)], +) -> Option<(i32, i32)> { + if candidates.is_empty() || desired_w <= 0 || desired_h <= 0 { + return candidates.first().copied(); + } + + let dw = desired_w as f64; + let dh = desired_h as f64; + let desired_aspect = dw / dh; + let desired_area = (desired_w as i64) * (desired_h as i64); + + let mut best: Option<((i32, i32), f64, i64)> = None; + + for &(w, h) in candidates { + let wf = w as f64; + let hf = h as f64; + + let a1 = wf / hf; + let a2 = hf / wf; + let aspect_diff = (a1 - desired_aspect).abs().min((a2 - desired_aspect).abs()); + + let area = (w as i64) * (h as i64); + let area_diff = (area - desired_area).abs(); + + match best { + None => best = Some(((w, h), aspect_diff, area_diff)), + Some((_, best_ad, best_area)) => { + if aspect_diff < best_ad || (aspect_diff == best_ad && area_diff < best_area) { + best = Some(((w, h), aspect_diff, area_diff)); + } + }, + } + } + + best.map(|(s, _, _)| s) +} diff --git a/src/drivers/android/mod.rs b/src/drivers/android/mod.rs new file mode 100644 index 0000000..fce9c32 --- /dev/null +++ b/src/drivers/android/mod.rs @@ -0,0 +1,8 @@ +// This is free and unencumbered software released into the public domain. + +pub mod devices; + +mod driver; +pub use driver::try_open; + +mod ndk; diff --git a/src/drivers/android/ndk/aimage.rs b/src/drivers/android/ndk/aimage.rs new file mode 100644 index 0000000..c3405bb --- /dev/null +++ b/src/drivers/android/ndk/aimage.rs @@ -0,0 +1,131 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys::{ + AImage, AImage_delete, AImage_getFormat, AImage_getHeight, AImage_getPlaneData, + AImage_getPlanePixelStride, AImage_getPlaneRowStride, AImage_getTimestamp, AImage_getWidth, + media_status_t, +}; + +use super::MediaResult; + +#[derive(Debug, Default)] +pub struct Image { + pub handle: *mut AImage, +} + +impl Image { + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn get_timestamp(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut result: i64 = 0; + let status = unsafe { AImage_getTimestamp(self.handle, &mut result) }; + if status != media_status_t::AMEDIA_OK { + return Err(status.into()); + } + Ok(result) + } + + pub fn get_width(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + let mut v: i32 = 0; + let st = unsafe { AImage_getWidth(self.handle, &mut v) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + Ok(v.max(0) as u32) + } + + pub fn get_height(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + let mut v: i32 = 0; + let st = unsafe { AImage_getHeight(self.handle, &mut v) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + Ok(v.max(0) as u32) + } + + pub fn get_format(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + let mut v: i32 = 0; + let st = unsafe { AImage_getFormat(self.handle, &mut v) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + Ok(v) + } + + pub fn get_plane_row_stride(&self, plane: i32) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + let mut v: i32 = 0; + let st = unsafe { AImage_getPlaneRowStride(self.handle, plane, &mut v) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + Ok(v) + } + + pub fn get_plane_pixel_stride(&self, plane: i32) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + let mut v: i32 = 0; + let st = unsafe { AImage_getPlanePixelStride(self.handle, plane, &mut v) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + Ok(v) + } + + pub fn copy_plane_data(&self, plane: i32) -> MediaResult> { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut data_ptr: *mut u8 = core::ptr::null_mut(); + let mut len: i32 = 0; + + let st = unsafe { AImage_getPlaneData(self.handle, plane, &mut data_ptr, &mut len) }; + if st != media_status_t::AMEDIA_OK { + return Err(st.into()); + } + if data_ptr.is_null() || len <= 0 { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let n = len as usize; + let mut out = Vec::with_capacity(n); + unsafe { + out.set_len(n); + core::ptr::copy_nonoverlapping(data_ptr as *const u8, out.as_mut_ptr(), n); + } + Ok(out) + } +} + +impl Drop for Image { + fn drop(&mut self) { + unsafe { + if !self.handle.is_null() { + AImage_delete(self.handle); + self.handle = null_mut(); + } + } + } +} diff --git a/src/drivers/android/ndk/camera_capture_session.rs b/src/drivers/android/ndk/camera_capture_session.rs new file mode 100644 index 0000000..ad9ee5a --- /dev/null +++ b/src/drivers/android/ndk/camera_capture_session.rs @@ -0,0 +1,128 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::CameraResult; +use super::camera_device::CameraDevice; +use super::camera_status::CameraStatus; + +#[derive(Debug)] +pub struct CameraCaptureSession { + handle: *mut ndk::ACameraCaptureSession, + _state_callbacks: Box, +} + +impl CameraCaptureSession { + pub fn open( + device: &CameraDevice, + outputs: *mut ndk::ACaptureSessionOutputContainer, + mut state_callbacks: Box, + ) -> CameraResult { + if device.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + if outputs.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut handle: *mut ndk::ACameraCaptureSession = null_mut(); + + let st = unsafe { + ndk::ACameraDevice_createCaptureSession( + device.as_ptr(), + outputs, + state_callbacks.as_mut(), + &mut handle, + ) + }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || handle.is_null() { + return Err(stw); + } + + Ok(Self { + handle, + _state_callbacks: state_callbacks, + }) + } + + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACameraCaptureSession { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn set_repeating_request( + &self, + request: *mut ndk::ACaptureRequest, + out_sequence_id: Option<&mut i32>, + ) -> CameraResult { + if self.handle.is_null() || request.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut seq_id_tmp: i32 = 0; + let seq_ptr: *mut i32 = match out_sequence_id { + Some(r) => r as *mut i32, + None => &mut seq_id_tmp as *mut i32, + }; + + let mut reqs: [*mut ndk::ACaptureRequest; 1] = [request]; + + let st = unsafe { + ndk::ACameraCaptureSession_setRepeatingRequest( + self.handle, + null_mut(), + reqs.len() as i32, + reqs.as_mut_ptr(), + seq_ptr, + ) + }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() { + return Err(stw); + } + Ok(()) + } + + pub fn stop_repeating(&self) -> CameraResult { + if self.handle.is_null() { + return Ok(()); + } + let st = unsafe { ndk::ACameraCaptureSession_stopRepeating(self.handle) }; + let stw = CameraStatus::from(st); + if !stw.is_ok() { + return Err(stw); + } + Ok(()) + } + + pub fn close(&mut self) { + if self.handle.is_null() { + return; + } + unsafe { + ndk::ACameraCaptureSession_close(self.handle); + } + self.handle = null_mut(); + } +} + +impl Drop for CameraCaptureSession { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/ndk/camera_device.rs b/src/drivers/android/ndk/camera_device.rs new file mode 100644 index 0000000..443783b --- /dev/null +++ b/src/drivers/android/ndk/camera_device.rs @@ -0,0 +1,44 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +#[derive(Debug)] +pub struct CameraDevice { + handle: *mut ndk::ACameraDevice, + _state_callbacks: Box, +} + +impl CameraDevice { + #[inline] + pub fn new( + handle: *mut ndk::ACameraDevice, + state_callbacks: Box, + ) -> Self { + Self { + handle, + _state_callbacks: state_callbacks, + } + } + + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACameraDevice { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } +} + +impl Drop for CameraDevice { + fn drop(&mut self) { + unsafe { + if !self.handle.is_null() { + ndk::ACameraDevice_close(self.handle); + self.handle = null_mut(); + } + } + } +} diff --git a/src/drivers/android/ndk/camera_manager.rs b/src/drivers/android/ndk/camera_manager.rs new file mode 100644 index 0000000..4007fbe --- /dev/null +++ b/src/drivers/android/ndk/camera_manager.rs @@ -0,0 +1,144 @@ +// This is free and unencumbered software released into the public domain. + +use core::ffi::c_char; +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::camera_device::CameraDevice; +use super::camera_status::{CameraResult, CameraStatus}; + +#[derive(Debug)] +pub struct CameraManager { + handle: *mut ndk::ACameraManager, +} + +#[derive(Debug)] +pub struct CameraIdList { + handle: *mut ndk::ACameraIdList, +} + +impl CameraIdList { + #[inline] + pub fn len(&self) -> usize { + if self.handle.is_null() { + return 0; + } + unsafe { (*self.handle).numCameras as usize } + } + + #[inline] + pub fn id_ptr(&self, idx: usize) -> *const c_char { + if self.handle.is_null() { + return core::ptr::null(); + } + unsafe { + let list_ref = &*self.handle; + if idx >= (list_ref.numCameras as usize) { + return core::ptr::null(); + } + *list_ref.cameraIds.add(idx) as *const c_char + } + } +} + +impl Drop for CameraIdList { + fn drop(&mut self) { + unsafe { + if !self.handle.is_null() { + ndk::ACameraManager_deleteCameraIdList(self.handle); + self.handle = null_mut(); + } + } + } +} + +impl CameraManager { + pub fn new() -> CameraResult { + let handle = unsafe { ndk::ACameraManager_create() }; + if handle.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_UNKNOWN, + )); + } + Ok(Self { handle }) + } + + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACameraManager { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn open_camera( + &self, + camera_id: *const c_char, + mut callbacks: Box, + ) -> CameraResult { + if self.handle.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_CAMERA_DISCONNECTED, + )); + } + if camera_id.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut dev_ptr: *mut ndk::ACameraDevice = null_mut(); + + let st = unsafe { + ndk::ACameraManager_openCamera(self.handle, camera_id, callbacks.as_mut(), &mut dev_ptr) + }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || dev_ptr.is_null() { + return Err(stw); + } + + Ok(CameraDevice::new(dev_ptr, callbacks)) + } + + pub fn get_characteristics( + &self, + camera_id: *const c_char, + out_metadata: *mut *mut ndk::ACameraMetadata, + ) -> ndk::camera_status_t { + unsafe { + ndk::ACameraManager_getCameraCharacteristics(self.handle, camera_id, out_metadata) + } + } + + pub fn list_camera_ids(&self) -> CameraResult { + if self.handle.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_CAMERA_DISCONNECTED, + )); + } + + let mut id_list: *mut ndk::ACameraIdList = null_mut(); + let st = unsafe { ndk::ACameraManager_getCameraIdList(self.handle, &mut id_list) }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || id_list.is_null() { + return Err(stw); + } + + Ok(CameraIdList { handle: id_list }) + } +} + +impl Drop for CameraManager { + fn drop(&mut self) { + unsafe { + if !self.handle.is_null() { + ndk::ACameraManager_delete(self.handle); + self.handle = null_mut(); + } + } + } +} diff --git a/src/drivers/android/ndk/camera_output_target.rs b/src/drivers/android/ndk/camera_output_target.rs new file mode 100644 index 0000000..bf5607b --- /dev/null +++ b/src/drivers/android/ndk/camera_output_target.rs @@ -0,0 +1,57 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::camera_status::{CameraResult, CameraStatus}; + +#[derive(Debug, Default)] +pub struct CameraOutputTarget { + handle: *mut ndk::ACameraOutputTarget, +} + +impl CameraOutputTarget { + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACameraOutputTarget { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn new(window: *mut ndk::ANativeWindow) -> CameraResult { + if window.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut out = Self::default(); + let st = unsafe { ndk::ACameraOutputTarget_create(window, &mut out.handle) }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || out.handle.is_null() { + return Err(stw); + } + + Ok(out) + } + + pub fn close(&mut self) { + if self.handle.is_null() { + return; + } + unsafe { + ndk::ACameraOutputTarget_free(self.handle); + } + self.handle = null_mut(); + } +} + +impl Drop for CameraOutputTarget { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/ndk/camera_status.rs b/src/drivers/android/ndk/camera_status.rs new file mode 100644 index 0000000..75dba46 --- /dev/null +++ b/src/drivers/android/ndk/camera_status.rs @@ -0,0 +1,73 @@ +// This is free and unencumbered software released into the public domain. + +use core::fmt; +use ndk_sys::camera_status_t; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CameraStatus(pub camera_status_t); + +pub type CameraResult = core::result::Result; + +impl CameraStatus { + #[inline] + pub const fn ok() -> Self { + Self(camera_status_t::ACAMERA_OK) + } + + #[inline] + pub const fn code(self) -> i32 { + (self.0).0 + } + + #[inline] + pub fn is_ok(self) -> bool { + self.0 == camera_status_t::ACAMERA_OK + } + + #[inline] + pub fn is_disconnected(self) -> bool { + self.0 == camera_status_t::ACAMERA_ERROR_CAMERA_DISCONNECTED + } + + #[inline] + pub fn is_not_enough_memory(self) -> bool { + self.0 == camera_status_t::ACAMERA_ERROR_NOT_ENOUGH_MEMORY + } + + #[inline] + pub fn is_permission_denied(self) -> bool { + self.0 == camera_status_t::ACAMERA_ERROR_PERMISSION_DENIED + } + + #[inline] + pub fn is_camera_in_use(self) -> bool { + self.0 == camera_status_t::ACAMERA_ERROR_CAMERA_IN_USE + } + + #[inline] + pub fn is_max_cameras_in_use(self) -> bool { + self.0 == camera_status_t::ACAMERA_ERROR_MAX_CAMERA_IN_USE + } +} + +impl Default for CameraStatus { + #[inline] + fn default() -> Self { + Self::ok() + } +} + +impl From for CameraStatus { + #[inline] + fn from(s: camera_status_t) -> Self { + Self(s) + } +} + +impl fmt::Display for CameraStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "camera_status={}", self.code()) + } +} + +impl core::error::Error for CameraStatus {} diff --git a/src/drivers/android/ndk/capture_request.rs b/src/drivers/android/ndk/capture_request.rs new file mode 100644 index 0000000..2eaf104 --- /dev/null +++ b/src/drivers/android/ndk/capture_request.rs @@ -0,0 +1,78 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::camera_device::CameraDevice; +use super::camera_output_target::CameraOutputTarget; +use super::camera_status::{CameraResult, CameraStatus}; + +#[derive(Debug, Default)] +pub struct CaptureRequest { + handle: *mut ndk::ACaptureRequest, +} + +impl CaptureRequest { + pub fn new( + device: &CameraDevice, + template: ndk::ACameraDevice_request_template, + ) -> CameraResult { + if device.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut out = Self::default(); + + let st = unsafe { + ndk::ACameraDevice_createCaptureRequest(device.as_ptr(), template, &mut out.handle) + }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || out.handle.is_null() { + return Err(stw); + } + + Ok(out) + } + + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACaptureRequest { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn add_target(&self, target: &CameraOutputTarget) -> CameraResult { + if self.handle.is_null() || target.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let st = unsafe { ndk::ACaptureRequest_addTarget(self.handle, target.as_ptr()) }; + let stw = CameraStatus::from(st); + if !stw.is_ok() { + return Err(stw); + } + Ok(()) + } + + pub fn close(&mut self) { + if self.handle.is_null() { + return; + } + unsafe { ndk::ACaptureRequest_free(self.handle) }; + self.handle = null_mut(); + } +} + +impl Drop for CaptureRequest { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/ndk/capture_session_output.rs b/src/drivers/android/ndk/capture_session_output.rs new file mode 100644 index 0000000..fc2ba56 --- /dev/null +++ b/src/drivers/android/ndk/capture_session_output.rs @@ -0,0 +1,55 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::camera_status::{CameraResult, CameraStatus}; + +#[derive(Debug, Default)] +pub struct CaptureSessionOutput { + handle: *mut ndk::ACaptureSessionOutput, +} + +impl CaptureSessionOutput { + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACaptureSessionOutput { + self.handle + } + + #[inline] + pub fn is_null(&self) -> bool { + self.handle.is_null() + } + + pub fn new(window: *mut ndk::ANativeWindow) -> CameraResult { + if window.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let mut out = Self::default(); + let st = unsafe { ndk::ACaptureSessionOutput_create(window, &mut out.handle) }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || out.handle.is_null() { + return Err(stw); + } + + Ok(out) + } + + pub fn close(&mut self) { + if self.handle.is_null() { + return; + } + unsafe { ndk::ACaptureSessionOutput_free(self.handle) }; + self.handle = null_mut(); + } +} + +impl Drop for CaptureSessionOutput { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/ndk/capture_session_output_container.rs b/src/drivers/android/ndk/capture_session_output_container.rs new file mode 100644 index 0000000..b985e18 --- /dev/null +++ b/src/drivers/android/ndk/capture_session_output_container.rs @@ -0,0 +1,62 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +use super::camera_status::{CameraResult, CameraStatus}; +use super::capture_session_output::CaptureSessionOutput; + +#[derive(Debug, Default)] +pub struct CaptureSessionOutputContainer { + handle: *mut ndk::ACaptureSessionOutputContainer, +} + +impl CaptureSessionOutputContainer { + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ACaptureSessionOutputContainer { + self.handle + } + + pub fn new() -> CameraResult { + let mut out = Self::default(); + let st = unsafe { ndk::ACaptureSessionOutputContainer_create(&mut out.handle) }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() || out.handle.is_null() { + return Err(stw); + } + + Ok(out) + } + + pub fn add(&self, output: &CaptureSessionOutput) -> CameraResult { + if self.handle.is_null() || output.is_null() { + return Err(CameraStatus::from( + ndk::camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER, + )); + } + + let st = unsafe { ndk::ACaptureSessionOutputContainer_add(self.handle, output.as_ptr()) }; + + let stw = CameraStatus::from(st); + if !stw.is_ok() { + return Err(stw); + } + + Ok(()) + } + + pub fn close(&mut self) { + if self.handle.is_null() { + return; + } + unsafe { ndk::ACaptureSessionOutputContainer_free(self.handle) }; + self.handle = null_mut(); + } +} + +impl Drop for CaptureSessionOutputContainer { + fn drop(&mut self) { + self.close(); + } +} diff --git a/src/drivers/android/ndk/image_reader.rs b/src/drivers/android/ndk/image_reader.rs new file mode 100644 index 0000000..2eca3b9 --- /dev/null +++ b/src/drivers/android/ndk/image_reader.rs @@ -0,0 +1,160 @@ +// This is free and unencumbered software released into the public domain. + +use core::ffi::c_void; +use core::ptr::null_mut; + +use ndk_sys::{ + AImageReader, AImageReader_ImageListener, AImageReader_acquireLatestImage, AImageReader_delete, + AImageReader_getFormat, AImageReader_getHeight, AImageReader_getWidth, AImageReader_getWindow, + AImageReader_new, AImageReader_setImageListener, ANativeWindow, media_status_t, +}; + +use super::{Image, MediaResult, MediaStatus, NativeWindow}; + +#[derive(Debug)] +pub struct ImageReader { + handle: *mut AImageReader, + listener: Option>, +} + +impl Default for ImageReader { + fn default() -> Self { + Self { + handle: null_mut(), + listener: None, + } + } +} + +impl ImageReader { + pub fn new(dimensions: (u32, u32), format: i32, max_images: i32) -> MediaResult { + let (width, height) = dimensions; + + let mut this = Self::default(); + let status = unsafe { + AImageReader_new( + width as _, + height as _, + format, + max_images, + &mut this.handle, + ) + }; + + if status != media_status_t::AMEDIA_OK || this.handle.is_null() { + return Err(MediaStatus::from(status)); + } + + Ok(this) + } + + #[inline] + pub fn as_ptr(&self) -> *mut AImageReader { + self.handle + } + + pub fn get_window(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut win: *mut ANativeWindow = null_mut(); + let status = unsafe { AImageReader_getWindow(self.handle, &mut win) }; + if status != media_status_t::AMEDIA_OK { + return Err(status.into()); + } + if win.is_null() { + return Err(media_status_t::AMEDIA_ERROR_UNKNOWN.into()); + } + + Ok(NativeWindow::from_ptr_borrowed(win)) + } + + pub fn set_image_listener( + &mut self, + context: *mut c_void, + cb: extern "C" fn(*mut c_void, *mut AImageReader), + ) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut listener = Box::new(AImageReader_ImageListener { + context, + onImageAvailable: Some(cb), + }); + + let status = unsafe { AImageReader_setImageListener(self.handle, listener.as_mut()) }; + if status != media_status_t::AMEDIA_OK { + return Err(status.into()); + } + + self.listener = Some(listener); + Ok(()) + } + + pub fn acquire_latest_image(&self) -> MediaResult { + unsafe { acquire_latest_image_from_raw(self.handle) } + } + + pub fn get_width(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut result = 0; + let status = unsafe { AImageReader_getWidth(self.handle, &mut result) }; + if status != media_status_t::AMEDIA_OK { + return Err(status.into()); + } + Ok(result as _) + } + + pub fn get_height(&self) -> MediaResult { + if self.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut result = 0; + let status = unsafe { AImageReader_getHeight(self.handle, &mut result) }; + if status != media_status_t::AMEDIA_OK { + return Err(status.into()); + } + Ok(result as _) + } + + pub fn close(&mut self) { + unsafe { + if !self.handle.is_null() { + AImageReader_delete(self.handle); + self.handle = null_mut(); + } + } + self.listener = None; + } +} + +impl Drop for ImageReader { + fn drop(&mut self) { + self.close(); + } +} + +pub unsafe fn acquire_latest_image_from_raw(reader: *mut AImageReader) -> MediaResult { + if reader.is_null() { + return Err(media_status_t::AMEDIA_ERROR_INVALID_PARAMETER.into()); + } + + let mut result = Image::default(); + + let status = unsafe { AImageReader_acquireLatestImage(reader, &mut result.handle) }; + + if status != media_status_t::AMEDIA_OK { + return Err(MediaStatus::from(status)); + } + if result.handle.is_null() { + return Err(media_status_t::AMEDIA_ERROR_UNKNOWN.into()); + } + + Ok(result) +} diff --git a/src/drivers/android/ndk/media_status.rs b/src/drivers/android/ndk/media_status.rs new file mode 100644 index 0000000..c198f78 --- /dev/null +++ b/src/drivers/android/ndk/media_status.rs @@ -0,0 +1,58 @@ +// This is free and unencumbered software released into the public domain. + +use core::fmt; +use ndk_sys::media_status_t; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MediaStatus(pub media_status_t); + +pub type MediaResult = core::result::Result; + +impl MediaStatus { + #[inline] + pub const fn ok() -> Self { + Self(media_status_t::AMEDIA_OK) + } + + #[inline] + pub fn is_ok(self) -> bool { + self.0 == media_status_t::AMEDIA_OK + } + + #[inline] + pub fn is_would_block(self) -> bool { + self.0 == media_status_t::AMEDIA_ERROR_WOULD_BLOCK + } + + #[inline] + pub fn is_max_images_acquired(self) -> bool { + self.0 == media_status_t::AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED + } + + #[inline] + pub fn is_no_buffer_available(self) -> bool { + self.0 == media_status_t::AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE + } +} + +impl Default for MediaStatus { + #[inline] + fn default() -> Self { + Self::ok() + } +} + +impl From for MediaStatus { + #[inline] + fn from(s: media_status_t) -> Self { + Self(s) + } +} + +impl fmt::Display for MediaStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "media_status={:?}", self.0) + } +} + +impl core::error::Error for MediaStatus {} diff --git a/src/drivers/android/ndk/mod.rs b/src/drivers/android/ndk/mod.rs new file mode 100644 index 0000000..333d3ad --- /dev/null +++ b/src/drivers/android/ndk/mod.rs @@ -0,0 +1,37 @@ +// This is free and unencumbered software released into the public domain. + +mod camera_capture_session; +pub use camera_capture_session::*; + +mod camera_device; +pub use camera_device::*; + +mod camera_manager; +pub use camera_manager::*; + +mod camera_status; +pub use camera_status::*; + +mod media_status; +pub use media_status::*; + +mod camera_output_target; +pub use camera_output_target::*; + +mod capture_request; +pub use capture_request::*; + +mod capture_session_output; +pub use capture_session_output::*; + +mod capture_session_output_container; +pub use capture_session_output_container::*; + +mod aimage; +pub use aimage::*; + +mod image_reader; +pub use image_reader::*; + +mod native_window; +pub use native_window::*; diff --git a/src/drivers/android/ndk/native_window.rs b/src/drivers/android/ndk/native_window.rs new file mode 100644 index 0000000..76ad743 --- /dev/null +++ b/src/drivers/android/ndk/native_window.rs @@ -0,0 +1,90 @@ +// This is free and unencumbered software released into the public domain. + +use core::ptr::null_mut; +use ndk_sys as ndk; + +#[derive(Debug)] +pub struct NativeWindow { + handle: *mut ndk::ANativeWindow, + owned: bool, +} + +impl Default for NativeWindow { + fn default() -> Self { + Self { + handle: null_mut(), + owned: false, + } + } +} + +impl NativeWindow { + #[inline] + pub fn from_ptr_borrowed(ptr: *mut ndk::ANativeWindow) -> Self { + Self { + handle: ptr, + owned: false, + } + } + + #[inline] + pub fn from_ptr_owned(ptr: *mut ndk::ANativeWindow) -> Self { + Self { + handle: ptr, + owned: true, + } + } + + pub fn acquire(&mut self) { + if self.handle.is_null() || self.owned { + return; + } + unsafe { ndk::ANativeWindow_acquire(self.handle) }; + self.owned = true; + } + + #[inline] + pub fn as_ptr(&self) -> *mut ndk::ANativeWindow { + self.handle + } + + pub fn width(&self) -> i32 { + if self.handle.is_null() { + return 0; + } + unsafe { ndk::ANativeWindow_getWidth(self.handle) } + } + + pub fn height(&self) -> i32 { + if self.handle.is_null() { + return 0; + } + unsafe { ndk::ANativeWindow_getHeight(self.handle) } + } + + pub fn format(&self) -> i32 { + if self.handle.is_null() { + return 0; + } + unsafe { ndk::ANativeWindow_getFormat(self.handle) } + } + + pub fn set_buffers_geometry(&mut self, width: i32, height: i32, format: i32) -> i32 { + if self.handle.is_null() { + return -1; + } + unsafe { ndk::ANativeWindow_setBuffersGeometry(self.handle, width, height, format) } + } +} + +impl Drop for NativeWindow { + fn drop(&mut self) { + unsafe { + if self.owned && !self.handle.is_null() { + ndk::ANativeWindow_release(self.handle); + self.handle = null_mut(); + self.owned = false; + } + } + } +} diff --git a/src/drivers/avf/devices.rs b/src/drivers/avf/devices.rs new file mode 100644 index 0000000..9ccb341 --- /dev/null +++ b/src/drivers/avf/devices.rs @@ -0,0 +1,68 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraError, DeviceInfo, DeviceKind}; + +pub fn list_video_devices() -> Result, CameraError> { + use objc2::rc::Retained; + use objc2_av_foundation::{ + AVCaptureDevice, AVCaptureDeviceDiscoverySession, AVCaptureDevicePosition, + AVCaptureDeviceType, AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeExternal, AVMediaTypeVideo, + }; + use objc2_foundation::{NSArray, NSString}; + + unsafe { + let built_in = AVCaptureDeviceTypeBuiltInWideAngleCamera; + let external = AVCaptureDeviceTypeExternal; + + let device_types: Retained> = + NSArray::from_slice(&[built_in, external]); + + let media = AVMediaTypeVideo.expect("AVMediaTypeVideo is unavailable"); + + let session: Retained = + AVCaptureDeviceDiscoverySession::discoverySessionWithDeviceTypes_mediaType_position( + &device_types, + Some(media), + AVCaptureDevicePosition::Unspecified, + ); + + let devices: Retained> = session.devices(); + let mut out = Vec::with_capacity(devices.count() as usize); + + for i in 0..devices.count() { + let dev = devices.objectAtIndex(i); + + let uid: Retained = dev.uniqueID(); + let id = uid.to_string(); + + let lname: Retained = dev.localizedName(); + let name = lname.to_string(); + + let dtype: Retained = dev.deviceType(); + let pos = dev.position(); + + let is_external = *dtype == *external; + + let kind = if is_external { + DeviceKind::External + } else { + match pos { + AVCaptureDevicePosition::Front => DeviceKind::Front, + AVCaptureDevicePosition::Back => DeviceKind::Back, + _ => { + if *dtype == *built_in { + DeviceKind::Front + } else { + DeviceKind::Unknown + } + }, + } + }; + + out.push(DeviceInfo::new(id.clone(), name, kind)); + } + + Ok(out) + } +} diff --git a/src/drivers/avf/driver.rs b/src/drivers/avf/driver.rs new file mode 100644 index 0000000..e35d28a --- /dev/null +++ b/src/drivers/avf/driver.rs @@ -0,0 +1,504 @@ +// This is free and unencumbered software released into the public domain. + +#![allow(dead_code, unused_imports)] + +use crate::drivers::CameraDriver; +use crate::{CameraBackend, CameraError, RawFrame, RawFrameRef}; + +use core::ffi::c_void; +use core::ptr::NonNull; + +use dispatch2::{DispatchQueue, MainThreadBound}; +use objc2::exception::catch; +use objc2::runtime::ProtocolObject; +use objc2::{ + AnyThread, DefinedClass, MainThreadMarker, Message, define_class, msg_send, rc::Retained, +}; +use objc2_av_foundation::{ + AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceDiscoverySession, AVCaptureDeviceInput, + AVCaptureDevicePosition, AVCaptureDeviceType, AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeExternal, AVCaptureOutput, AVCaptureSession, AVCaptureVideoDataOutput, + AVCaptureVideoDataOutputSampleBufferDelegate, AVMediaTypeVideo, +}; +use objc2_core_media::{CMSampleBuffer, CMTime}; +use objc2_core_video::{ + CVPixelBufferGetBaseAddress, CVPixelBufferGetBytesPerRow, CVPixelBufferGetDataSize, + CVPixelBufferGetHeight, CVPixelBufferGetWidth, CVPixelBufferLockBaseAddress, + CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress, +}; +use objc2_foundation::{ + NSArray, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSString, ns_string, +}; + +use std::fmt; +use std::panic::AssertUnwindSafe; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crossbeam_channel as ch; +use crossbeam_channel::{Receiver, Sender}; + +use crate::drivers::DriverConfig; + +#[derive(Debug)] +struct NotMainThread; + +impl fmt::Display for NotMainThread { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AVFoundation must be initialized on the main thread") + } +} + +impl std::error::Error for NotMainThread {} + +/// `MainThreadBound::get_on_main` requires the return value to be `Send`. +/// Raw pointers are not `Send`, so we wrap it in a newtype and mark it `Send`. +#[derive(Copy, Clone)] +struct SessionPtr(*mut c_void); +unsafe impl Send for SessionPtr {} + +#[inline] +fn objc_catch_unit(context: &'static str, f: impl FnOnce()) -> Result<(), CameraError> { + match catch(AssertUnwindSafe(f)) { + Ok(()) => Ok(()), + Err(_) => Err(CameraError::other(format!( + "Objective-C exception while {context}" + ))), + } +} + +#[inline] +fn objc_catch_value(context: &'static str, f: impl FnOnce() -> T) -> Result { + match catch(AssertUnwindSafe(f)) { + Ok(v) => Ok(v), + Err(_) => Err(CameraError::other(format!( + "Objective-C exception while {context}" + ))), + } +} + +pub struct AvfDriver { + _cfg: DriverConfig, + + _raw_tx: Sender, + raw_rx: Receiver, + + session: Option>>, + delegate: Option>, + _queue: Option>, + + running: bool, + closed: bool, +} + +pub fn try_open(cfg: &DriverConfig) -> Result, CameraError> { + AvfDriver::open(cfg).map(|d| Box::new(d) as Box) +} + +impl AvfDriver { + pub fn open(cfg: &DriverConfig) -> Result { + let cfg = cfg.clone(); + cfg.validate()?; + + let cap = cfg.buffer_raw.max(1).min(32); + let (raw_tx, raw_rx) = ch::bounded::(cap); + + let mtm = MainThreadMarker::new() + .ok_or_else(|| CameraError::driver("initializing AVFoundation", NotMainThread))?; + + let (session, delegate, queue) = + objc_catch_value("creating/configuring AVCaptureSession", || unsafe { + let session = AVCaptureSession::new(); + let (delegate, queue) = Self::configure_session(&session, &cfg, raw_tx.clone())?; + Ok::<_, CameraError>((session, delegate, queue)) + })??; + + let session = MainThreadBound::new(session, mtm); + + Ok(Self { + _cfg: cfg, + _raw_tx: raw_tx, + raw_rx, + session: Some(session), + delegate: Some(delegate), + _queue: Some(queue), + running: false, + closed: false, + }) + } + + unsafe fn configure_session( + session: &AVCaptureSession, + cfg: &DriverConfig, + raw_tx: Sender, + ) -> Result<(Retained, Retained), CameraError> { + unsafe { session.beginConfiguration() }; + + let result = + (|| -> Result<(Retained, Retained), CameraError> { + let device = Self::find_device(cfg)?; + + let _ = objc_catch_unit("applying device format/fps configuration", || unsafe { + let _ = Self::apply_configuration_to_device(&device, cfg); + }); + + let input = + unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) } + .map_err(|_| CameraError::other("AVCaptureDeviceInput creation failed"))?; + + if unsafe { !session.canAddInput(&input) } { + return Err(CameraError::other("AVCaptureSession cannot add input")); + } + unsafe { session.addInput(&input) }; + + let output = unsafe { AVCaptureVideoDataOutput::new() }; + + { + let key = ns_string!("PixelFormatType"); + let value = NSNumber::new_i32(i32::from_be_bytes(*b"BGRA")); + let settings = NSDictionary::::from_slices(&[key], &[&value]); + unsafe { output.setVideoSettings(Some(&*settings)) }; + } + + unsafe { output.setAlwaysDiscardsLateVideoFrames(true) }; + + let queue = DispatchQueue::new("asimov.camera.avf.queue", None); + let queue: Retained = queue.into(); + + let delegate = AvfCameraDelegate::new(raw_tx); + + let protocol_obj = ProtocolObject::from_ref(&*delegate); + unsafe { output.setSampleBufferDelegate_queue(Some(protocol_obj), Some(&*queue)) }; + + if unsafe { !session.canAddOutput(&output) } { + return Err(CameraError::other("AVCaptureSession cannot add output")); + } + unsafe { session.addOutput(&output) }; + + Ok((delegate, queue)) + })(); + + unsafe { session.commitConfiguration() }; + result + } + + fn find_device(cfg: &DriverConfig) -> Result, CameraError> { + let wanted_id = cfg.device.id(); + let wanted_name = cfg.device.name(); + + if wanted_id.trim().is_empty() && wanted_name.trim().is_empty() { + return unsafe { + AVCaptureDevice::defaultDeviceWithMediaType(AVMediaTypeVideo.unwrap().as_ref()) + } + .ok_or(CameraError::NoCamera); + } + + let device_types: Retained> = unsafe { + let built_in = AVCaptureDeviceTypeBuiltInWideAngleCamera; + let external = AVCaptureDeviceTypeExternal; + NSArray::from_slice(&[built_in, external]) + }; + + let discovery: Retained = unsafe { + AVCaptureDeviceDiscoverySession::discoverySessionWithDeviceTypes_mediaType_position( + &device_types, + Some(AVMediaTypeVideo.expect("AVMediaTypeVideo is unavailable")), + AVCaptureDevicePosition::Unspecified, + ) + }; + + let devices: Retained> = unsafe { discovery.devices() }; + + if !wanted_id.trim().is_empty() { + for dev in devices.iter() { + let uid: Retained = unsafe { dev.uniqueID() }; + if uid.to_string() == wanted_id { + return Ok(dev.retain()); + } + } + } + + if !wanted_name.trim().is_empty() { + for dev in devices.iter() { + let lname: Retained = unsafe { dev.localizedName() }; + if lname.to_string() == wanted_name { + return Ok(dev.retain()); + } + } + } + + Err(CameraError::NoCamera) + } + + unsafe fn apply_configuration_to_device( + device: &AVCaptureDevice, + cfg: &DriverConfig, + ) -> Result<(), CameraError> { + if cfg.width == 0 || cfg.height == 0 { + return Ok(()); + } + + if unsafe { device.lockForConfiguration() }.is_err() { + return Ok(()); + } + + let res = (|| -> Result<(), CameraError> { + let formats = unsafe { device.formats() }; + let mut best_format = None; + + for format in formats.iter() { + let desc = unsafe { format.formatDescription() }; + let dims = + unsafe { objc2_core_media::CMVideoFormatDescriptionGetDimensions(&desc) }; + + if dims.width as u32 != cfg.width || dims.height as u32 != cfg.height { + continue; + } + + for range in unsafe { format.videoSupportedFrameRateRanges() } { + let max_rate = unsafe { range.maxFrameRate() }; + if cfg.fps <= 0.0 || max_rate >= cfg.fps { + best_format = Some(format); + break; + } + } + + if best_format.is_some() { + break; + } + } + + if let Some(fmt) = best_format { + unsafe { device.setActiveFormat(&fmt) }; + + if cfg.fps.is_finite() && cfg.fps > 0.0 { + let fps_i32 = cfg.fps.round().max(1.0).min(i32::MAX as f64) as i32; + let duration = unsafe { CMTime::new(1, fps_i32) }; + unsafe { device.setActiveVideoMinFrameDuration(duration) }; + unsafe { device.setActiveVideoMaxFrameDuration(duration) }; + } + } + + Ok(()) + })(); + + unsafe { device.unlockForConfiguration() }; + res + } + + fn teardown(&mut self) { + if self.closed { + return; + } + self.closed = true; + + let _ = self.stop(); + + self.delegate = None; + self._queue = None; + self.session = None; + } +} + +impl Drop for AvfDriver { + fn drop(&mut self) { + self.teardown(); + } +} + +impl CameraDriver for AvfDriver { + fn backend(&self) -> CameraBackend { + CameraBackend::Avf + } + + fn start(&mut self) -> Result<(), CameraError> { + if self.closed { + return Err(CameraError::Closed); + } + if self.running { + return Ok(()); + } + + let Some(ref session) = self.session else { + return Err(CameraError::NotConfigured); + }; + + session.get_on_main(|s| { + let _ = catch(AssertUnwindSafe(|| unsafe { + s.startRunning(); + })); + }); + + self.running = true; + Ok(()) + } + + fn stop(&mut self) -> Result<(), CameraError> { + if self.closed { + return Ok(()); + } + if !self.running { + return Ok(()); + } + + if let Some(ref session) = self.session { + session.get_on_main(|s| { + let _ = catch(AssertUnwindSafe(|| unsafe { + s.stopRunning(); + })); + }); + } + + self.running = false; + Ok(()) + } + + fn close(&mut self) -> Result<(), CameraError> { + self.teardown(); + Ok(()) + } + + fn read_frames(&mut self) -> Result, CameraError> { + if self.closed { + return Err(CameraError::Closed); + } + Ok(self.raw_rx.clone()) + } + + #[cfg(all(feature = "mobile-preview", feature = "avf", target_os = "ios"))] + fn session_handle(&self) -> Result { + let Some(ref session) = self.session else { + return Err(CameraError::NotConfigured); + }; + + let ptr = session + .get_on_main(|s: &Retained| { + let p: *const AVCaptureSession = &**s; + SessionPtr(p as *mut c_void) + }) + .0; + + let nn = + NonNull::new(ptr).ok_or_else(|| CameraError::other("AVCaptureSession ptr is null"))?; + Ok(unsafe { crate::AvfSessionHandle::from_nonnull_ptr(nn) }) + } +} + +define_class!( + #[unsafe(super(NSObject))] + #[name = "AvfCameraDelegate"] + #[ivars = AvfCameraDelegateVars] + #[derive(Debug)] + struct AvfCameraDelegate; + + unsafe impl NSObjectProtocol for AvfCameraDelegate {} + + unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for AvfCameraDelegate { + #[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))] + unsafe fn capture_output_did_output_sample_buffer_from_connection( + &self, + _capture_output: &AVCaptureOutput, + sample_buffer: &CMSampleBuffer, + _connection: &AVCaptureConnection, + ) { + let _ = catch(AssertUnwindSafe(|| { + let image_buffer = unsafe { CMSampleBuffer::image_buffer(sample_buffer) }; + let Some(pixel_buffer) = image_buffer else { + return; + }; + + if unsafe { + CVPixelBufferLockBaseAddress(&pixel_buffer, CVPixelBufferLockFlags::ReadOnly) + } != 0 + { + return; + } + + let did_lock = true; + let _ = did_lock; + + let res = (|| { + let width = CVPixelBufferGetWidth(&pixel_buffer) as u32; + let height = CVPixelBufferGetHeight(&pixel_buffer) as u32; + let stride = CVPixelBufferGetBytesPerRow(&pixel_buffer) as u32; + + let base = CVPixelBufferGetBaseAddress(&pixel_buffer); + let size = CVPixelBufferGetDataSize(&pixel_buffer); + + if base.is_null() || size == 0 || width == 0 || height == 0 { + return; + } + + let mut out = Vec::::with_capacity(size); + unsafe { + out.set_len(size); + core::ptr::copy_nonoverlapping(base as *const u8, out.as_mut_ptr(), size); + } + + let ts = unsafe { CMSampleBuffer::presentation_time_stamp(sample_buffer) }; + let timestamp_ns = cm_time_to_ns(ts); + let raw = RawFrame::new_bgra8(width, height, out, stride, Some(timestamp_ns)); + + let frame_ref: RawFrameRef = Arc::new(raw); + + let vars = self.ivars(); + let _ = vars.raw_tx.try_send(frame_ref); + vars.frame_counter.fetch_add(1, Ordering::Relaxed); + })(); + + unsafe { + CVPixelBufferUnlockBaseAddress(&pixel_buffer, CVPixelBufferLockFlags::ReadOnly) + }; + res + })); + } + } +); + +impl AvfCameraDelegate { + fn new(raw_tx: Sender) -> Retained { + let this = Self::alloc().set_ivars(AvfCameraDelegateVars { + raw_tx, + frame_counter: AtomicU64::new(0), + }); + unsafe { msg_send![super(this), init] } + } +} + +pub struct AvfCameraDelegateVars { + pub raw_tx: Sender, + pub frame_counter: AtomicU64, +} + +impl Clone for AvfCameraDelegateVars { + fn clone(&self) -> Self { + panic!("AvfCameraDelegateVars cannot be cloned"); + } +} + +impl fmt::Debug for AvfCameraDelegateVars { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AvfCameraDelegateVars {{ ... }}") + } +} + +fn cm_time_to_ns(t: CMTime) -> u64 { + let ts = t.timescale; + if ts <= 0 { + return 0; + } + + let value = t.value; + if value <= 0 { + return 0; + } + + let value_u128 = value as u128; + let ts_u128 = ts as u128; + + let ns = value_u128 + .saturating_mul(1_000_000_000u128) + .saturating_div(ts_u128); + + ns.min(u64::MAX as u128) as u64 +} diff --git a/src/drivers/avf/mod.rs b/src/drivers/avf/mod.rs new file mode 100644 index 0000000..b8f7b33 --- /dev/null +++ b/src/drivers/avf/mod.rs @@ -0,0 +1,6 @@ +// This is free and unencumbered software released into the public domain. + +pub mod devices; + +mod driver; +pub use driver::try_open; diff --git a/src/drivers/config.rs b/src/drivers/config.rs new file mode 100644 index 0000000..22d0017 --- /dev/null +++ b/src/drivers/config.rs @@ -0,0 +1,63 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraError, DeviceInfo}; + +#[derive(Clone, Debug)] +pub struct DriverConfig { + pub device: DeviceInfo, + pub width: u32, + pub height: u32, + pub fps: f64, + pub buffer_raw: usize, + #[allow(dead_code)] + pub diagnostics: bool, + + /// Android preview target (ANativeWindow*) is required when building with + /// `mobile-preview` on Android. + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + pub android_preview: crate::AndroidPreviewTarget, +} + +impl DriverConfig { + pub fn normalized(mut self) -> Self { + self.width = self.width.max(1); + self.height = self.height.max(1); + + self.fps = if self.fps.is_finite() && self.fps > 0.0 { + self.fps + } else { + 30.0 + }; + + self.buffer_raw = self.buffer_raw.max(1); + + self + } + + pub fn validate(&self) -> Result<(), CameraError> { + let id = self.device.id().trim(); + if id.is_empty() { + return Err(CameraError::invalid_config("driver device id is empty")); + } + if self.width == 0 || self.height == 0 { + return Err(CameraError::invalid_config("width/height must be > 0")); + } + if !self.fps.is_finite() || self.fps <= 0.0 { + return Err(CameraError::invalid_config("fps must be finite and > 0")); + } + if self.buffer_raw == 0 { + return Err(CameraError::invalid_config("buffer_raw must be >= 1")); + } + + #[cfg(all(feature = "mobile-preview", feature = "android", target_os = "android"))] + { + if self.android_preview.as_ptr().is_null() { + return Err(CameraError::invalid_config( + "android_preview must be a non-null native window pointer", + )); + } + } + + Ok(()) + } +} diff --git a/src/drivers/ffmpeg/devices.rs b/src/drivers/ffmpeg/devices.rs new file mode 100644 index 0000000..9cf759e --- /dev/null +++ b/src/drivers/ffmpeg/devices.rs @@ -0,0 +1,325 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraError, DeviceInfo, DeviceKind}; + +pub fn list_video_devices() -> Result, CameraError> { + #[cfg(target_os = "macos")] + { + return ffmpeg_list_devices_macos_avfoundation(); + } + #[cfg(target_os = "windows")] + { + return ffmpeg_list_devices_windows_dshow(); + } + #[cfg(target_os = "linux")] + { + return ffmpeg_list_devices_linux_v4l2(); + } + + #[allow(unreachable_code)] + Ok(Vec::new()) +} + +fn ffmpeg_bin() -> String { + std::env::var("OPENPACK_FFMPEG_BIN") + .ok() + .filter(|s| !s.trim().is_empty()) + .or_else(|| { + std::env::var("FFMPEG_BIN") + .ok() + .filter(|s| !s.trim().is_empty()) + }) + .unwrap_or_else(|| "ffmpeg".to_string()) +} + +fn run_ffmpeg(args: &[&str]) -> Result<(i32, String, String), CameraError> { + use std::process::Command; + + let bin = ffmpeg_bin(); + let out = Command::new(&bin).args(args).output().map_err(|e| { + CameraError::driver( + "ffmpeg", + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("failed to execute '{bin}': {e}"), + ), + ) + })?; + + let code = out.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&out.stdout).to_string(); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + Ok((code, stdout, stderr)) +} + +fn ffmpeg_list_devices_macos_avfoundation() -> Result, CameraError> { + let (_code, _stdout, stderr) = run_ffmpeg(&[ + "-hide_banner", + "-f", + "avfoundation", + "-list_devices", + "true", + "-i", + "", + ])?; + + let usb_names = macos_usb_product_names(); + + let mut in_video = false; + let mut out = Vec::new(); + + for line in stderr.lines() { + if line.contains("AVFoundation video devices:") { + in_video = true; + continue; + } + if line.contains("AVFoundation audio devices:") { + in_video = false; + continue; + } + if !in_video { + continue; + } + + if let Some(pos) = line.rfind("] [") { + let tail = &line[pos + 3..]; + if let Some(end) = tail.find(']') { + let idx = tail[..end].trim(); + let name = tail[end + 1..].trim(); + if idx.is_empty() || name.is_empty() { + continue; + } + + let is_usb = usb_names + .iter() + .any(|u| !u.is_empty() && contains_case_insensitive(name, u)); + + out.push(DeviceInfo::new( + idx.to_string(), + name.to_string(), + if is_usb { + DeviceKind::External + } else { + DeviceKind::Unknown + }, + )); + } + } + } + + if out.is_empty() { + return Err(CameraError::other( + "no video devices were returned by ffmpeg (avfoundation)", + )); + } + + Ok(out) +} + +fn contains_case_insensitive(haystack: &str, needle: &str) -> bool { + haystack.to_lowercase().contains(&needle.to_lowercase()) +} + +fn macos_usb_product_names() -> Vec { + let out = std::process::Command::new("ioreg") + .args(["-p", "IOUSB", "-l"]) + .output(); + + let Ok(out) = out else { + return Vec::new(); + }; + if !out.status.success() { + return Vec::new(); + } + + let s = String::from_utf8_lossy(&out.stdout); + let mut names = Vec::new(); + + for line in s.lines() { + let line = line.trim(); + if let Some(v) = extract_quoted_value(line, "\"USB Product Name\"") { + names.push(v); + } else if let Some(v) = extract_quoted_value(line, "\"kUSBProductString\"") { + names.push(v); + } + } + + names.sort(); + names.dedup(); + names +} + +fn extract_quoted_value(line: &str, key: &str) -> Option { + if !line.contains(key) { + return None; + } + let eq = line.find('=')?; + let rhs = line[eq + 1..].trim(); + let first = rhs.find('"')?; + let rest = &rhs[first + 1..]; + let last = rest.find('"')?; + Some(rest[..last].to_string()) +} + +#[cfg(target_os = "windows")] +fn ffmpeg_list_devices_windows_dshow() -> Result, CameraError> { + let (_code, _stdout, stderr) = run_ffmpeg(&[ + "-hide_banner", + "-f", + "dshow", + "-list_devices", + "true", + "-i", + "dummy", + ])?; + + let mut in_video = false; + let mut out = Vec::new(); + + for line in stderr.lines() { + let s = line.trim(); + + if s.contains("DirectShow video devices") { + in_video = true; + continue; + } + if s.contains("DirectShow audio devices") { + in_video = false; + continue; + } + if !in_video { + continue; + } + + if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 { + let name = &s[1..s.len() - 1]; + if name.is_empty() { + continue; + } + + let n = name.to_lowercase(); + let is_ext = n.contains("usb") + || n.contains("webcam") + || n.contains("uvc") + || n.contains("capture"); + + out.push(DeviceInfo::new( + name.to_string(), + name.to_string(), + if is_ext { + DeviceKind::External + } else { + DeviceKind::Unknown + }, + )); + } + } + + if out.is_empty() { + return Err(CameraError::other( + "no video devices were returned by ffmpeg (dshow)", + )); + } + + Ok(out) +} + +#[cfg(target_os = "linux")] +fn ffmpeg_list_devices_linux_v4l2() -> Result, CameraError> { + use std::fs; + use std::path::{Path, PathBuf}; + + fn sysfs_video_dir(idx: u32) -> PathBuf { + PathBuf::from("/sys/class/video4linux").join(format!("video{idx}")) + } + + fn sysfs_name(sys: &Path, fallback: &str) -> String { + fs::read_to_string(sys.join("name")) + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| fallback.to_string()) + } + + fn sysfs_is_usb(sys: &Path) -> bool { + let dev = sys.join("device"); + if let Ok(link) = fs::read_link(&dev) { + if link.to_string_lossy().to_lowercase().contains("usb") { + return true; + } + } + if let Ok(uevent) = fs::read_to_string(dev.join("uevent")) { + if uevent.to_lowercase().contains("usb") { + return true; + } + } + false + } + + let mut idxs: Vec = Vec::new(); + if let Ok(entries) = fs::read_dir("/dev") { + for e in entries.flatten() { + let name = e.file_name(); + let s = name.to_string_lossy(); + if s.starts_with("video") && s[5..].chars().all(|c| c.is_ascii_digit()) { + if let Ok(v) = s[5..].parse::() { + idxs.push(v); + } + } + } + } + idxs.sort_unstable(); + idxs.dedup(); + + let mut out = Vec::new(); + + for idx in idxs { + let devnode = format!("/dev/video{idx}"); + if !Path::new(&devnode).exists() { + continue; + } + + let (_code, _stdout, stderr) = run_ffmpeg(&[ + "-hide_banner", + "-loglevel", + "error", + "-f", + "v4l2", + "-i", + &devnode, + "-t", + "0.1", + "-frames:v", + "1", + "-f", + "null", + "-", + ])?; + + if stderr.contains("No such file") || stderr.contains("No such device") { + continue; + } + + let sys = sysfs_video_dir(idx); + let name = sysfs_name(&sys, &devnode); + let is_usb = sysfs_is_usb(&sys); + + out.push(DeviceInfo::new( + devnode.clone(), + name, + if is_usb { + DeviceKind::External + } else { + DeviceKind::Unknown + }, + )); + } + + if out.is_empty() { + return Err(CameraError::other( + "no usable v4l2 video devices were found via ffmpeg", + )); + } + + Ok(out) +} diff --git a/src/drivers/ffmpeg/driver.rs b/src/drivers/ffmpeg/driver.rs new file mode 100644 index 0000000..17599f0 --- /dev/null +++ b/src/drivers/ffmpeg/driver.rs @@ -0,0 +1,341 @@ +// This is free and unencumbered software released into the public domain. + +use std::io::Read; +use std::process::{Child, Command, Stdio}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; +use std::thread::JoinHandle; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use bytes::Bytes; +use crossbeam_channel as ch; + +use crate::drivers::{CameraDriver, DriverConfig}; +use crate::{CameraBackend, CameraError, RawFormat, RawFrame, RawFrameRef, RawPlane}; + +pub struct FfmpegDriver { + cfg: DriverConfig, + + raw_tx: ch::Sender, + raw_rx: ch::Receiver, + + child: Option>>, + stop: Arc, + running: AtomicBool, + closed: AtomicBool, + + reader_join: Option>, + monitor_join: Option>, +} + +pub fn try_open(cfg: &DriverConfig) -> Result, CameraError> { + Ok(Box::new(FfmpegDriver::open(cfg)?)) +} + +impl core::fmt::Debug for FfmpegDriver { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FfmpegDriver") + .field("cfg", &self.cfg) + .field("child", &self.child.as_ref().map(|_| "")) + .field("running", &self.running.load(Ordering::Relaxed)) + .field("closed", &self.closed.load(Ordering::Relaxed)) + .finish() + } +} + +impl FfmpegDriver { + pub fn open(cfg: &DriverConfig) -> Result { + cfg.validate()?; + + let cap = cfg.buffer_raw.max(1).min(8); + let (raw_tx, raw_rx) = ch::bounded::(cap); + + Ok(Self { + cfg: cfg.clone(), + raw_tx, + raw_rx, + child: None, + stop: Arc::new(AtomicBool::new(false)), + running: AtomicBool::new(false), + closed: AtomicBool::new(false), + reader_join: None, + monitor_join: None, + }) + } + + #[inline] + fn now_ns_best_effort() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) + } + + fn spawn_ffmpeg(&self) -> Result { + let device_id = self.cfg.device.id().trim(); + if device_id.is_empty() { + return Err(CameraError::invalid_config("ffmpeg device id is empty")); + } + + let input_device = get_input_device(device_id); + + #[cfg(target_os = "macos")] + let input_fps: f64 = 30.0; + + #[cfg(not(target_os = "macos"))] + let input_fps: f64 = { + let fps = if self.cfg.fps.is_finite() && self.cfg.fps > 0.1 { + self.cfg.fps + } else { + 30.0 + }; + fps.min(240.0) + }; + + let mut ffargs: Vec = vec![ + "-hide_banner".into(), + "-nostdin".into(), + "-nostats".into(), + "-f".into(), + ffmpeg_format().into(), + "-loglevel".into(), + "error".into(), + "-video_size".into(), + format!("{}x{}", self.cfg.width, self.cfg.height), + "-framerate".into(), + format!("{input_fps}"), + ]; + + #[cfg(target_os = "macos")] + { + ffargs.push("-pixel_format".into()); + ffargs.push("0rgb".into()); + } + + ffargs.extend([ + "-i".into(), + input_device, + "-pix_fmt".into(), + "rgb24".into(), + "-f".into(), + "rawvideo".into(), + "pipe:1".into(), + ]); + + let stderr = + if self.cfg.diagnostics || std::env::var_os("ASIMOV_CAMERA_FFMPEG_STDERR").is_some() { + Stdio::inherit() + } else { + Stdio::null() + }; + + Command::new("ffmpeg") + .args(&ffargs) + .stdout(Stdio::piped()) + .stderr(stderr) + .spawn() + .map_err(|e| CameraError::driver("spawning ffmpeg", e)) + } + + fn stop_child(&mut self) { + let Some(child_arc) = self.child.take() else { + return; + }; + let mut g = child_arc.lock().unwrap_or_else(|p| p.into_inner()); + terminate_child(&mut *g); + } + + fn join_threads(&mut self) { + if let Some(j) = self.reader_join.take() { + let _ = j.join(); + } + if let Some(j) = self.monitor_join.take() { + let _ = j.join(); + } + } +} + +impl Drop for FfmpegDriver { + fn drop(&mut self) { + let _ = self.close(); + } +} + +impl CameraDriver for FfmpegDriver { + fn backend(&self) -> CameraBackend { + CameraBackend::Ffmpeg + } + + fn start(&mut self) -> Result<(), CameraError> { + if self.closed.load(Ordering::Acquire) { + return Err(CameraError::Closed); + } + if self.running.swap(true, Ordering::AcqRel) { + return Ok(()); + } + if self.child.is_some() { + return Ok(()); + } + + self.stop.store(false, Ordering::Release); + + let mut child = self.spawn_ffmpeg()?; + let stdout = child + .stdout + .take() + .ok_or_else(|| CameraError::other("ffmpeg stdout not piped"))?; + + let width = self.cfg.width; + let height = self.cfg.height; + + let row_stride = width.saturating_mul(3); + let frame_size = (row_stride as usize).saturating_mul(height as usize); + + let child_arc = Arc::new(Mutex::new(child)); + self.child = Some(Arc::clone(&child_arc)); + + let stop = Arc::clone(&self.stop); + let raw_tx = self.raw_tx.clone(); + + let raw_rx_drop = self.raw_rx.clone(); + + let reader_join = std::thread::Builder::new() + .name("asimov-ffmpeg-reader".to_string()) + .spawn(move || { + let mut reader = std::io::BufReader::new(stdout); + let mut buf = vec![0u8; frame_size]; + + while !stop.load(Ordering::Acquire) { + match reader.read_exact(&mut buf) { + Ok(()) => { + let ts = FfmpegDriver::now_ns_best_effort(); + + let plane = RawPlane::new(Bytes::copy_from_slice(&buf), row_stride, 3); + + let frame_ref: RawFrameRef = Arc::new(RawFrame { + width, + height, + format: RawFormat::PackedRgb8, + planes: vec![plane], + timestamp_ns: Some(ts), + }); + + if raw_tx.try_send(Arc::clone(&frame_ref)).is_err() { + let _ = raw_rx_drop.try_recv(); + let _ = raw_tx.try_send(frame_ref); + } + }, + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + break; + }, + Err(_) => { + break; + }, + } + } + }) + .map_err(|e| CameraError::other(format!("failed to spawn ffmpeg reader: {e}")))?; + + let stop2 = Arc::clone(&self.stop); + let child_arc2 = Arc::clone(&child_arc); + + let monitor_join = std::thread::Builder::new() + .name("asimov-ffmpeg-monitor".to_string()) + .spawn(move || { + while !stop2.load(Ordering::Acquire) { + let status = { + let mut g = child_arc2.lock().unwrap_or_else(|p| p.into_inner()); + g.try_wait() + }; + + match status { + Ok(Some(_)) => break, + Ok(None) => std::thread::sleep(Duration::from_millis(150)), + Err(_) => break, + } + } + }) + .map_err(|e| CameraError::other(format!("failed to spawn ffmpeg monitor: {e}")))?; + + self.reader_join = Some(reader_join); + self.monitor_join = Some(monitor_join); + + Ok(()) + } + + fn stop(&mut self) -> Result<(), CameraError> { + self.stop.store(true, Ordering::Release); + + self.stop_child(); + + self.join_threads(); + + self.running.store(false, Ordering::Release); + Ok(()) + } + + fn close(&mut self) -> Result<(), CameraError> { + if self.closed.swap(true, Ordering::AcqRel) { + return Ok(()); + } + let _ = self.stop(); + Ok(()) + } + + fn read_frames(&mut self) -> Result, CameraError> { + if self.closed.load(Ordering::Acquire) { + return Err(CameraError::Closed); + } + Ok(self.raw_rx.clone()) + } +} + +fn terminate_child(child: &mut Child) { + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_millis(200) { + if let Ok(Some(_)) = child.try_wait() { + return; + } + std::thread::sleep(Duration::from_millis(20)); + } + + let _ = child.kill(); + let _ = child.wait(); +} + +#[cfg(target_os = "macos")] +fn ffmpeg_format() -> &'static str { + "avfoundation" +} + +#[cfg(target_os = "linux")] +fn ffmpeg_format() -> &'static str { + "v4l2" +} + +#[cfg(target_os = "windows")] +fn ffmpeg_format() -> &'static str { + "dshow" +} + +#[cfg(target_os = "macos")] +fn get_input_device(device: &str) -> String { + device.strip_prefix("avf:").unwrap_or(device).to_string() +} + +#[cfg(target_os = "linux")] +fn get_input_device(device: &str) -> String { + let d = device.strip_prefix("file:").unwrap_or(device); + if d.chars().all(|c| c.is_ascii_digit()) { + format!("/dev/video{d}") + } else { + d.to_string() + } +} + +#[cfg(target_os = "windows")] +fn get_input_device(device: &str) -> String { + device.strip_prefix("dshow:").unwrap_or(device).to_string() +} diff --git a/src/drivers/ffmpeg/mod.rs b/src/drivers/ffmpeg/mod.rs new file mode 100644 index 0000000..18d78a4 --- /dev/null +++ b/src/drivers/ffmpeg/mod.rs @@ -0,0 +1,7 @@ +// This is free and unencumbered software released into the public domain. + +#[cfg(any(not(target_os = "macos"), not(feature = "avf")))] +pub mod devices; + +mod driver; +pub use driver::try_open; diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs new file mode 100644 index 0000000..76b4a06 --- /dev/null +++ b/src/drivers/mod.rs @@ -0,0 +1,88 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{CameraBackend, CameraError, RawFrameRef}; + +#[cfg(all(feature = "android", target_os = "android"))] +pub mod android; + +#[cfg(all(feature = "avf", any(target_os = "ios", target_os = "macos")))] +pub mod avf; + +#[cfg(all( + feature = "ffmpeg", + any(target_os = "macos", target_os = "windows", target_os = "linux") +))] +pub mod ffmpeg; + +mod config; +pub use config::*; + +pub trait CameraDriver { + fn backend(&self) -> CameraBackend; + + fn start(&mut self) -> Result<(), CameraError>; + fn stop(&mut self) -> Result<(), CameraError>; + fn close(&mut self) -> Result<(), CameraError>; + fn read_frames(&mut self) -> Result, CameraError>; + + #[inline] + fn preview_info(&self) -> Option<(u32, u32, i32)> { + None + } + + #[cfg(all(feature = "mobile-preview", feature = "avf", target_os = "ios"))] + fn session_handle(&self) -> Result { + Err(CameraError::unsupported( + "session_handle is not supported by this driver", + )) + } +} + +#[inline] +fn try_driver( + res: Result, CameraError>, +) -> Result>, CameraError> { + match res { + Ok(d) => Ok(Some(d)), + Err(e) if e.is_not_applicable() || matches!(e, CameraError::NoDriver) => Ok(None), + Err(e) => Err(e), + } +} + +pub fn open(cfg: &DriverConfig) -> Result, CameraError> { + cfg.validate()?; + + #[cfg(all(feature = "android", target_os = "android"))] + { + return match try_driver(android::try_open(cfg))? { + Some(d) => Ok(d), + None => Err(CameraError::NoDriver), + }; + } + + #[cfg(all(feature = "avf", any(target_os = "ios", target_os = "macos")))] + { + if let Some(d) = try_driver(avf::try_open(cfg))? { + return Ok(d); + } + + #[cfg(target_os = "ios")] + { + return Err(CameraError::NoDriver); + } + } + + #[cfg(all( + feature = "ffmpeg", + any(target_os = "macos", target_os = "windows", target_os = "linux") + ))] + { + return match try_driver(ffmpeg::try_open(cfg))? { + Some(d) => Ok(d), + None => Err(CameraError::NoDriver), + }; + } + + #[allow(unreachable_code)] + Err(CameraError::NoDriver) +} diff --git a/src/lib.rs b/src/lib.rs index 558ac6a..5f3de10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,13 @@ // This is free and unencumbered software released into the public domain. -extern crate alloc; +mod api; +pub use api::*; -#[cfg(feature = "cli")] -pub mod cli; -pub mod shared; +mod camera; +pub use camera::*; + +mod runtime; + +mod drivers; + +mod converter; diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs new file mode 100644 index 0000000..ccaf3ef --- /dev/null +++ b/src/runtime/mod.rs @@ -0,0 +1,3 @@ +// This is free and unencumbered software released into the public domain. + +pub mod sampler; diff --git a/src/runtime/sampler.rs b/src/runtime/sampler.rs new file mode 100644 index 0000000..c8f6330 --- /dev/null +++ b/src/runtime/sampler.rs @@ -0,0 +1,54 @@ +// This is free and unencumbered software released into the public domain. + +use std::time::{Duration, Instant}; + +#[derive(Debug)] +pub struct FpsSampler { + period: Duration, + next_deadline: Instant, +} + +impl FpsSampler { + pub fn new(fps: f64) -> Self { + let fps = if fps.is_finite() && fps > 0.0 { + fps + } else { + 30.0 + }; + let period = Duration::from_secs_f64(1.0 / fps); + let now = Instant::now(); + Self { + period, + next_deadline: now + period, + } + } + + /// Returns `true` if we should emit now (i.e., deadline reached), and advances the deadline. + #[inline] + pub fn should_emit(&mut self) -> bool { + let now = Instant::now(); + if now >= self.next_deadline { + self.advance_deadline(); + true + } else { + false + } + } + + #[inline] + #[allow(dead_code)] + pub fn time_until_deadline(&self) -> Duration { + let now = Instant::now(); + self.next_deadline.saturating_duration_since(now) + } + + #[inline] + pub fn advance_deadline(&mut self) { + let now = Instant::now(); + if now >= self.next_deadline { + self.next_deadline = now + self.period; + } else { + self.next_deadline += self.period; + } + } +} diff --git a/src/shared/config.rs b/src/shared/config.rs deleted file mode 100644 index 79d1329..0000000 --- a/src/shared/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::PixelFormat; - -#[derive(Clone, Debug)] -pub struct CameraConfig { - pub device: Option, - pub width: u32, - pub height: u32, - pub fps: f64, - pub pixel_format: Option, - pub buffer_frames: usize, - pub diagnostics: bool, -} - -impl Default for CameraConfig { - fn default() -> Self { - Self { - device: None, - width: 640, - height: 480, - fps: 30.0, - pixel_format: None, - buffer_frames: 2, - diagnostics: false, - } - } -} - -impl CameraConfig { - pub fn new(width: u32, height: u32, fps: f64) -> Self { - Self { - width, - height, - fps, - ..Default::default() - } - } - - pub fn with_device(mut self, device: impl Into) -> Self { - self.device = Some(device.into()); - self - } - - pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self { - self.pixel_format = Some(fmt); - self - } - - pub fn with_buffer_frames(mut self, n: usize) -> Self { - self.buffer_frames = n.max(1); - self - } - - pub fn with_diagnostics(mut self, enabled: bool) -> Self { - self.diagnostics = enabled; - self - } -} diff --git a/src/shared/driver.rs b/src/shared/driver.rs deleted file mode 100644 index fb3f416..0000000 --- a/src/shared/driver.rs +++ /dev/null @@ -1,209 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::{CameraError, Frame}; -use std::{ - any::Any, - sync::{ - Arc, RwLock, - mpsc::{Receiver, SyncSender, TrySendError, sync_channel}, - }, - thread::JoinHandle, -}; - -pub type FrameSink = Arc; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CameraBackend { - Android, - Avf, - Dshow, - V4l2, - Ffmpeg, -} - -#[derive(Debug)] -pub enum CameraEvent { - Started { - backend: CameraBackend, - }, - Stopped { - backend: CameraBackend, - }, - FrameDropped { - backend: CameraBackend, - }, - Warning { - backend: CameraBackend, - message: String, - }, - Error { - backend: CameraBackend, - error: CameraError, - }, -} - -pub enum FrameMsg { - Frame(Frame), - Stop, -} - -pub struct Dispatcher { - tx: SyncSender, - sinks: Arc>>, - join: Option>, -} - -impl Dispatcher { - pub fn new( - capacity: usize, - backend: CameraBackend, - events_tx: SyncSender, - ) -> Self { - let (tx, rx) = sync_channel::(capacity.max(1)); - let sinks: Arc>> = Arc::new(RwLock::new(Vec::new())); - let sinks_clone = Arc::clone(&sinks); - - let join = std::thread::spawn(move || { - let _ = events_tx.try_send(CameraEvent::Started { backend }); - - while let Ok(msg) = rx.recv() { - match msg { - FrameMsg::Frame(frame) => { - if let Ok(list) = sinks_clone.read() { - for s in list.iter() { - (s)(frame.clone()); - } - } - }, - FrameMsg::Stop => break, - } - } - - let _ = events_tx.try_send(CameraEvent::Stopped { backend }); - }); - - Self { - tx, - sinks, - join: Some(join), - } - } - - pub fn sender(&self) -> SyncSender { - self.tx.clone() - } - - pub fn add_sink(&self, sink: FrameSink) { - if let Ok(mut g) = self.sinks.write() { - g.push(sink); - } - } - - pub fn stop(&mut self) { - let _ = self.tx.try_send(FrameMsg::Stop); - if let Some(j) = self.join.take() { - let _ = j.join(); - } - } -} - -pub trait CameraDriver: Send { - fn backend(&self) -> CameraBackend; - fn start(&mut self) -> Result<(), CameraError>; - fn stop(&mut self) -> Result<(), CameraError> { - Ok(()) - } - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; -} - -pub struct Camera { - driver: Box, - dispatcher: Dispatcher, - events_rx: Receiver, -} - -impl Camera { - #[cfg_attr( - not(any( - all( - feature = "ffmpeg", - any(target_os = "macos", target_os = "linux", target_os = "windows") - ), - all(feature = "avf", any(target_os = "macos", target_os = "ios")), - all(feature = "android", target_os = "android"), - all(feature = "dshow", target_os = "windows"), - all(feature = "v4l2", target_os = "linux"), - )), - allow(dead_code) - )] - pub(crate) fn new( - driver: Box, - dispatcher: Dispatcher, - events_rx: Receiver, - ) -> Self { - Self { - driver, - dispatcher, - events_rx, - } - } - - pub fn backend(&self) -> CameraBackend { - self.driver.backend() - } - - pub fn add_sink(&self, sink: FrameSink) { - self.dispatcher.add_sink(sink); - } - - pub fn events(&self) -> &Receiver { - &self.events_rx - } - - pub fn start(&mut self) -> Result<(), CameraError> { - self.driver.start() - } - - pub fn stop(&mut self) -> Result<(), CameraError> { - let r = self.driver.stop(); - self.dispatcher.stop(); - r - } - - pub fn driver_as(&self) -> Option<&T> { - self.driver.as_any().downcast_ref::() - } - - pub fn driver_as_mut(&mut self) -> Option<&mut T> { - self.driver.as_any_mut().downcast_mut::() - } -} - -impl Drop for Camera { - fn drop(&mut self) { - let _ = self.stop(); - } -} - -pub fn report_drop(events_tx: &SyncSender, backend: CameraBackend) { - let _ = events_tx.try_send(CameraEvent::FrameDropped { backend }); -} - -pub fn try_send_frame( - frame_tx: &SyncSender, - events_tx: &SyncSender, - backend: CameraBackend, - frame: Frame, -) { - match frame_tx.try_send(FrameMsg::Frame(frame)) { - Ok(()) => {}, - Err(TrySendError::Full(_)) => report_drop(events_tx, backend), - Err(TrySendError::Disconnected(_)) => { - let _ = events_tx.try_send(CameraEvent::Error { - backend, - error: CameraError::Closed, - }); - }, - } -} diff --git a/src/shared/drivers/android.rs b/src/shared/drivers/android.rs deleted file mode 100644 index 645a0a2..0000000 --- a/src/shared/drivers/android.rs +++ /dev/null @@ -1,208 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -mod camera_capture_session; -pub use camera_capture_session::*; - -mod camera_device; -pub use camera_device::*; - -mod camera_manager; -pub use camera_manager::*; - -mod camera_output_target; -pub use camera_output_target::*; - -mod camera_status; -pub use camera_status::*; - -mod capture_request; -pub use capture_request::*; - -mod capture_session_output; -pub use capture_session_output::*; - -mod capture_session_output_container; -pub use capture_session_output_container::*; - -mod image; -pub use image::*; - -mod image_reader; -pub use image_reader::*; - -mod media_status; -pub use media_status::*; - -mod native_window; -pub use native_window::*; - -use crate::shared::{ - CameraBackend, CameraConfig, CameraDriver, CameraError, CameraEvent, Frame, FrameMsg, - try_send_frame, -}; -use alloc::{borrow::Cow, ffi::CString}; -use bytes::Bytes; -use core::{ffi::CStr, ptr::null_mut}; -use ndk_sys::{ - ACameraManager_create, ACameraManager_delete, ACameraManager_deleteCameraIdList, - ACameraManager_getCameraIdList, ACameraManager_openCamera, android_get_device_api_level, - camera_status_t, -}; -use scopeguard::defer; -use std::any::Any; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - mpsc::SyncSender, -}; - -#[link(name = "camera2ndk")] -unsafe extern "C" {} - -#[link(name = "mediandk")] -unsafe extern "C" {} - -#[link(name = "binder_ndk")] -unsafe extern "C" {} - -#[derive(Debug)] -pub struct AndroidCameraDriver { - pub config: CameraConfig, - pub api_level: u32, - #[allow(unused)] - pub(crate) device: CameraDevice, - #[allow(unused)] - pub(crate) session: Option, - - frame_tx: SyncSender, - events_tx: SyncSender, - running: Arc, -} - -impl dogma::Named for AndroidCameraDriver { - fn name(&self) -> Cow<'_, str> { - "camera2".into() - } -} - -impl Drop for AndroidCameraDriver { - fn drop(&mut self) { - let _ = self.stop(); - } -} - -impl AndroidCameraDriver { - pub fn open( - _input_url: impl AsRef, - config: CameraConfig, - frame_tx: SyncSender, - events_tx: SyncSender, - ) -> Result { - unsafe { - let api_level = android_get_device_api_level() as u32; - - let camera_manager = ACameraManager_create(); - defer! { - ACameraManager_delete(camera_manager); - } - - let mut camera_id_list_ptr = null_mut(); - let status = ACameraManager_getCameraIdList(camera_manager, &mut camera_id_list_ptr); - if status != camera_status_t::ACAMERA_OK { - return Err(CameraError::NoCamera); - } - defer! { - ACameraManager_deleteCameraIdList(camera_id_list_ptr); - } - - let camera_id_list = &*camera_id_list_ptr; - if camera_id_list.numCameras < 1 { - return Err(CameraError::NoCamera); - } - - let camera_ids = core::slice::from_raw_parts( - camera_id_list.cameraIds, - camera_id_list.numCameras as usize, - ); - let camera_id_strings: Vec = camera_ids - .iter() - .map(|p| CStr::from_ptr(*p).to_str().unwrap_or("").to_string()) - .collect(); - - if config.diagnostics { - let _ = events_tx.try_send(CameraEvent::Warning { - backend: CameraBackend::Android, - message: format!("ACameraManager_getCameraIdList={camera_id_strings:?}"), - }); - } - - let mut device = CameraDevice::default(); - let device_id = CString::new(camera_id_strings[0].clone()).unwrap(); - - let status = ACameraManager_openCamera( - camera_manager, - device_id.as_ptr(), - &mut device.state_callbacks, - &mut device.handle, - ); - - if config.diagnostics { - let _ = events_tx.try_send(CameraEvent::Warning { - backend: CameraBackend::Android, - message: format!("ACameraManager_openCamera status={status:?}"), - }); - } - - if status != camera_status_t::ACAMERA_OK { - return Err(CameraError::NoCamera); - } - - Ok(AndroidCameraDriver { - config, - api_level, - device, - session: None, - frame_tx, - events_tx, - running: Arc::new(AtomicBool::new(false)), - }) - } - } - - fn emit_frame(&self, frame: Frame) { - try_send_frame( - &self.frame_tx, - &self.events_tx, - CameraBackend::Android, - frame, - ); - } -} - -impl CameraDriver for AndroidCameraDriver { - fn backend(&self) -> CameraBackend { - CameraBackend::Android - } - - fn start(&mut self) -> Result<(), CameraError> { - let session_output_container = CaptureSessionOutputContainer::new().unwrap(); - self.session = - Some(CameraCaptureSession::open(&self.device, &session_output_container).unwrap()); // FIXME - - Err(CameraError::unsupported( - "android camera backend not implemented", - )) - } - - fn stop(&mut self) -> Result<(), CameraError> { - self.session = None; - Ok(()) - } - - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} diff --git a/src/shared/drivers/android/camera_capture_session.rs b/src/shared/drivers/android/camera_capture_session.rs deleted file mode 100644 index 3d38970..0000000 --- a/src/shared/drivers/android/camera_capture_session.rs +++ /dev/null @@ -1,130 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraDevice, CameraResult, CaptureRequest, CaptureSessionOutputContainer}; -use core::ffi::c_void; -use core::ptr::null_mut; -use ndk_sys::{ - ACameraCaptureSession, ACameraCaptureSession_capture, ACameraCaptureSession_close, - ACameraCaptureSession_setRepeatingRequest, ACameraCaptureSession_stateCallbacks, - ACameraCaptureSession_stopRepeating, ACameraDevice_createCaptureSession, camera_status_t, -}; - -#[derive(Clone, Debug)] -pub struct CameraCaptureSession { - handle: *mut ACameraCaptureSession, - state_callbacks: ACameraCaptureSession_stateCallbacks, -} - -impl Default for CameraCaptureSession { - fn default() -> Self { - Self { - handle: null_mut(), - state_callbacks: ACameraCaptureSession_stateCallbacks { - context: null_mut(), - onClosed: None, - onReady: None, - onActive: None, - }, - } - } -} - -impl Drop for CameraCaptureSession { - fn drop(&mut self) { - self.close() - } -} - -impl CameraCaptureSession { - pub fn open( - device: &CameraDevice, - outputs: &CaptureSessionOutputContainer, - ) -> CameraResult { - let mut result = Self::default(); - result.init(device, outputs)?; - Ok(result) - } - - fn init( - &mut self, - device: &CameraDevice, - outputs: &CaptureSessionOutputContainer, - ) -> CameraResult { - self.state_callbacks.context = (self as *mut _) as *mut c_void; - - unsafe extern "C" fn on_ready(_context: *mut c_void, session: *mut ACameraCaptureSession) { - eprintln!("CameraCaptureSession#on_ready: session={:?}", session); - } - self.state_callbacks.onReady = Some(on_ready); - - unsafe extern "C" fn on_active(_context: *mut c_void, session: *mut ACameraCaptureSession) { - eprintln!("CameraCaptureSession#on_active: session={:?}", session); - } - self.state_callbacks.onActive = Some(on_active); - - unsafe extern "C" fn on_closed(_context: *mut c_void, session: *mut ACameraCaptureSession) { - eprintln!("CameraCaptureSession#on_closed: session={:?}", session); - } - self.state_callbacks.onClosed = Some(on_closed); - - let status = unsafe { - ACameraDevice_createCaptureSession( - device.handle, - //self.output_container, - outputs.handle, - &self.state_callbacks, - &mut self.handle, - ) - }; - eprintln!("ACameraDevice_createCaptureSession={:?}", status); // DEBUG - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - - Ok(()) - } - - pub fn capture(&mut self, request: &CaptureRequest) -> CameraResult { - let mut requests = request.handle; - let status = unsafe { - ACameraCaptureSession_capture(self.handle, null_mut(), 1, &mut requests, null_mut()) - }; - eprintln!("ACameraCaptureSession_capture={:?}", status); // DEBUG - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(()) - } - - /// See: https://developer.android.com/ndk/reference/group/camera#acameracapturesession_setrepeatingrequest - pub fn set_repeating_request(&mut self, request: &CaptureRequest) -> CameraResult { - let mut requests = request.handle; - let status = unsafe { - ACameraCaptureSession_setRepeatingRequest( - self.handle, - null_mut(), - 1, - &mut requests, - null_mut(), - ) - }; - eprintln!("ACameraCaptureSession_setRepeatingRequest={:?}", status); // DEBUG - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(()) - } - - /// See: https://developer.android.com/ndk/reference/group/camera#acameracapturesession_stoprepeating - pub fn stop_repeating(&mut self) -> CameraResult { - let status = unsafe { ACameraCaptureSession_stopRepeating(self.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(()) - } - - pub fn close(&mut self) { - unsafe { ACameraCaptureSession_close(self.handle) } - } -} diff --git a/src/shared/drivers/android/camera_device.rs b/src/shared/drivers/android/camera_device.rs deleted file mode 100644 index 7ff9b2f..0000000 --- a/src/shared/drivers/android/camera_device.rs +++ /dev/null @@ -1,29 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use core::{mem::zeroed, ptr::null_mut}; -use ndk_sys::{ACameraDevice, ACameraDevice_StateCallbacks, ACameraDevice_close}; - -#[derive(Clone, Debug)] -pub struct CameraDevice { - pub(crate) handle: *mut ACameraDevice, - pub(crate) state_callbacks: ACameraDevice_StateCallbacks, -} - -impl Default for CameraDevice { - fn default() -> Self { - Self { - handle: null_mut(), - state_callbacks: unsafe { zeroed() }, - } - } -} - -impl Drop for CameraDevice { - fn drop(&mut self) { - unsafe { - // See: https://developer.android.com/ndk/reference/group/camera#acameradevice_close - ACameraDevice_close(self.handle); - self.handle = null_mut(); - } - } -} diff --git a/src/shared/drivers/android/camera_manager.rs b/src/shared/drivers/android/camera_manager.rs deleted file mode 100644 index 4fe51db..0000000 --- a/src/shared/drivers/android/camera_manager.rs +++ /dev/null @@ -1,80 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraDevice, CameraResult}; -use alloc::ffi::CString; -use core::{ffi::CStr, ptr::null_mut}; -//use core::{mem::zeroed, ptr::null_mut}; -use ndk_sys::{ - ACameraManager, ACameraManager_create, ACameraManager_delete, - ACameraManager_deleteCameraIdList, ACameraManager_getCameraIdList, ACameraManager_openCamera, - camera_status_t, -}; -use scopeguard::defer; - -#[derive(Debug, Default)] -pub struct CameraManager { - pub(crate) handle: *mut ACameraManager, -} - -impl Drop for CameraManager { - fn drop(&mut self) { - unsafe { - ACameraManager_delete(self.handle); - self.handle = null_mut(); - } - } -} - -impl CameraManager { - pub fn new() -> Self { - Self { - handle: unsafe { ACameraManager_create() }, - } - } - - pub fn get_camera_ids(&self) -> CameraResult> { - let mut list_ptr = null_mut(); - let status = unsafe { ACameraManager_getCameraIdList(self.handle, &mut list_ptr) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - - defer! { - unsafe { ACameraManager_deleteCameraIdList(list_ptr); } - } - - let list = unsafe { &*list_ptr }; - if list.numCameras < 1 { - return Ok(Vec::new()); // no devices - } - - let ids = unsafe { core::slice::from_raw_parts(list.cameraIds, list.numCameras as usize) }; - - let result: Vec = ids - .iter() - .map(|p| unsafe { CStr::from_ptr(*p).to_str().unwrap_or("").to_string() }) - .collect(); - - Ok(result) - } - - pub fn open_camera(&self, id: impl AsRef) -> CameraResult { - let id = CString::new(String::from(id.as_ref())).unwrap(); - - let mut device = CameraDevice::default(); - let status = unsafe { - ACameraManager_openCamera( - self.handle, - id.as_ptr(), - &mut device.state_callbacks, - &mut device.handle, - ) - }; - eprintln!("ACameraManager_openCamera={:?}", status); // DEBUG - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - - Ok(device) - } -} diff --git a/src/shared/drivers/android/camera_output_target.rs b/src/shared/drivers/android/camera_output_target.rs deleted file mode 100644 index 64b83f8..0000000 --- a/src/shared/drivers/android/camera_output_target.rs +++ /dev/null @@ -1,33 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraResult, NativeWindow}; -use core::ptr::null_mut; -use ndk_sys::{ - ACameraOutputTarget, ACameraOutputTarget_create, ACameraOutputTarget_free, camera_status_t, -}; - -#[derive(Debug, Default)] -pub struct CameraOutputTarget { - pub(crate) handle: *mut ACameraOutputTarget, -} - -impl Drop for CameraOutputTarget { - fn drop(&mut self) { - unsafe { - ACameraOutputTarget_free(self.handle); - self.handle = null_mut(); - } - } -} - -impl CameraOutputTarget { - pub fn new(window: &NativeWindow) -> CameraResult { - // See: https://developer.android.com/ndk/reference/group/camera#acameraoutputtarget_create - let mut result = Self::default(); - let status = unsafe { ACameraOutputTarget_create(window.handle, &mut result.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(result) - } -} diff --git a/src/shared/drivers/android/camera_status.rs b/src/shared/drivers/android/camera_status.rs deleted file mode 100644 index 512a06f..0000000 --- a/src/shared/drivers/android/camera_status.rs +++ /dev/null @@ -1,26 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use derive_more::Display; -use ndk_sys::camera_status_t; - -pub type CameraResult = core::result::Result; - -#[derive(Clone, Copy, Debug, Display)] -#[display("{}", _0.0)] -#[allow(unused)] -pub struct CameraStatus(pub(crate) camera_status_t); - -impl core::error::Error for CameraStatus {} - -impl Default for CameraStatus { - fn default() -> Self { - CameraStatus(camera_status_t::ACAMERA_OK) - } -} - -impl From for CameraStatus { - fn from(input: camera_status_t) -> Self { - assert!(input != camera_status_t::ACAMERA_ERROR_INVALID_PARAMETER); - Self(input) - } -} diff --git a/src/shared/drivers/android/capture_request.rs b/src/shared/drivers/android/capture_request.rs deleted file mode 100644 index 8c29d3e..0000000 --- a/src/shared/drivers/android/capture_request.rs +++ /dev/null @@ -1,52 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraDevice, CameraOutputTarget, CameraResult}; -use core::ptr::null_mut; -use ndk_sys::{ - ACameraDevice_createCaptureRequest, ACameraDevice_request_template, ACaptureRequest, - ACaptureRequest_addTarget, ACaptureRequest_free, camera_status_t, -}; - -#[derive(Debug, Default)] -pub struct CaptureRequest { - pub(crate) handle: *mut ACaptureRequest, -} - -impl Drop for CaptureRequest { - fn drop(&mut self) { - unsafe { ACaptureRequest_free(self.handle) } - self.handle = null_mut(); - } -} - -impl CaptureRequest { - pub fn new(device: &CameraDevice) -> CameraResult { - let mut result = Self::default(); - result.init(device)?; - Ok(result) - } - - fn init(&mut self, device: &CameraDevice) -> CameraResult { - let status = unsafe { - ACameraDevice_createCaptureRequest( - device.handle, - ACameraDevice_request_template::TEMPLATE_PREVIEW, - &mut self.handle, - ) - }; - eprintln!("ACameraDevice_createCaptureRequest={:?}", status); // DEBUG - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - - Ok(()) - } - - pub fn add_target(&mut self, target: &CameraOutputTarget) -> CameraResult { - let status = unsafe { ACaptureRequest_addTarget(self.handle, target.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(()) - } -} diff --git a/src/shared/drivers/android/capture_session_output.rs b/src/shared/drivers/android/capture_session_output.rs deleted file mode 100644 index a79f18a..0000000 --- a/src/shared/drivers/android/capture_session_output.rs +++ /dev/null @@ -1,34 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraResult, NativeWindow}; -use core::ptr::null_mut; -use ndk_sys::{ - ACaptureSessionOutput, ACaptureSessionOutput_create, ACaptureSessionOutput_free, - camera_status_t, -}; - -#[derive(Debug, Default)] -pub struct CaptureSessionOutput { - pub(crate) handle: *mut ACaptureSessionOutput, -} - -impl Drop for CaptureSessionOutput { - fn drop(&mut self) { - unsafe { - ACaptureSessionOutput_free(self.handle); - self.handle = null_mut(); - } - } -} - -impl CaptureSessionOutput { - pub fn new(window: &NativeWindow) -> CameraResult { - // See: https://developer.android.com/ndk/reference/group/camera#acapturesessionoutput_create - let mut result = Self::default(); - let status = unsafe { ACaptureSessionOutput_create(window.handle, &mut result.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(result) - } -} diff --git a/src/shared/drivers/android/capture_session_output_container.rs b/src/shared/drivers/android/capture_session_output_container.rs deleted file mode 100644 index 7d5136e..0000000 --- a/src/shared/drivers/android/capture_session_output_container.rs +++ /dev/null @@ -1,42 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{CameraResult, CaptureSessionOutput}; -use core::ptr::null_mut; -use ndk_sys::{ - ACaptureSessionOutputContainer, ACaptureSessionOutputContainer_add, - ACaptureSessionOutputContainer_create, ACaptureSessionOutputContainer_free, camera_status_t, -}; - -#[derive(Debug, Default)] -pub struct CaptureSessionOutputContainer { - pub(crate) handle: *mut ACaptureSessionOutputContainer, -} - -impl Drop for CaptureSessionOutputContainer { - fn drop(&mut self) { - // See: https://developer.android.com/ndk/reference/group/camera#acapturesessionoutputcontainer_free - unsafe { ACaptureSessionOutputContainer_free(self.handle) }; - self.handle = null_mut(); - } -} - -impl CaptureSessionOutputContainer { - pub fn new() -> CameraResult { - // See: https://developer.android.com/ndk/reference/group/camera#acapturesessionoutputcontainer_create - let mut result = Self::default(); - let status = unsafe { ACaptureSessionOutputContainer_create(&mut result.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(result) - } - - pub fn add(&mut self, target: &CaptureSessionOutput) -> CameraResult { - // See: https://developer.android.com/ndk/reference/group/camera#acapturesessionoutputcontainer_add - let status = unsafe { ACaptureSessionOutputContainer_add(self.handle, target.handle) }; - if status != camera_status_t::ACAMERA_OK { - return Err(status.into()); - } - Ok(()) - } -} diff --git a/src/shared/drivers/android/image.rs b/src/shared/drivers/android/image.rs deleted file mode 100644 index 8361fe2..0000000 --- a/src/shared/drivers/android/image.rs +++ /dev/null @@ -1,28 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::MediaResult; -use core::ptr::null_mut; -use ndk_sys::{AImage, AImage_delete, AImage_getTimestamp, media_status_t}; - -#[derive(Debug, Default)] -pub struct Image { - pub(crate) handle: *mut AImage, -} - -impl Drop for Image { - fn drop(&mut self) { - unsafe { AImage_delete(self.handle) } - self.handle = null_mut(); - } -} - -impl Image { - pub fn get_timestamp(&self) -> MediaResult { - let mut result = 0; - let status = unsafe { AImage_getTimestamp(self.handle, &mut result) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result as _) - } -} diff --git a/src/shared/drivers/android/image_reader.rs b/src/shared/drivers/android/image_reader.rs deleted file mode 100644 index cee9a65..0000000 --- a/src/shared/drivers/android/image_reader.rs +++ /dev/null @@ -1,133 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{Image, MediaResult, NativeWindow}; -use core::ffi::c_void; -use core::ptr::null_mut; -use ndk_sys::{ - AImageReader, AImageReader_ImageListener, AImageReader_acquireLatestImage, AImageReader_delete, - AImageReader_getFormat, AImageReader_getHeight, AImageReader_getWidth, AImageReader_getWindow, - AImageReader_new, AImageReader_setImageListener, media_status_t, -}; - -#[derive(Debug)] -pub struct ImageReader { - pub(crate) handle: *mut AImageReader, - pub(crate) image_listener: AImageReader_ImageListener, -} - -impl Default for ImageReader { - fn default() -> Self { - Self { - handle: null_mut(), - image_listener: AImageReader_ImageListener { - context: null_mut(), - onImageAvailable: None, - }, - } - } -} - -impl Drop for ImageReader { - fn drop(&mut self) { - unsafe { AImageReader_delete(self.handle) } - self.handle = null_mut(); - } -} - -impl ImageReader { - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_new - pub fn new(dimensions: (u32, u32), format: i32) -> MediaResult { - let (width, height) = dimensions; - let mut this = Self::default(); - - let status = - unsafe { AImageReader_new(width as _, height as _, format, 2, &mut this.handle) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - - unsafe extern "C" fn on_image_available( - _context: *mut c_void, - image_reader: *mut AImageReader, - ) { - eprintln!("ImageReader#on_image_available"); // TODO - let mut result = Image::default(); - let _status = - unsafe { AImageReader_acquireLatestImage(image_reader, &mut result.handle) }; - } - - let this_ptr: *mut ImageReader = &mut this as *mut _; - this.image_listener.context = this_ptr as *mut c_void; - this.image_listener.onImageAvailable = Some(on_image_available); - - let status = - unsafe { AImageReader_setImageListener(this.handle, &mut this.image_listener) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - - Ok(this) - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_getformat - pub fn get_format(&self) -> MediaResult { - let mut result = 0; - let status = unsafe { AImageReader_getFormat(self.handle, &mut result) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result) - } - - pub fn get_dimensions(&self) -> MediaResult<(u32, u32)> { - Ok((self.get_width()?, self.get_height()?)) - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_getwidth - pub fn get_width(&self) -> MediaResult { - let mut result = 0; - let status = unsafe { AImageReader_getWidth(self.handle, &mut result) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result as _) - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_getheight - pub fn get_height(&self) -> MediaResult { - let mut result = 0; - let status = unsafe { AImageReader_getHeight(self.handle, &mut result) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result as _) - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_getwindow - pub fn get_window(&self) -> MediaResult { - let mut result = NativeWindow { - owned: false, // the ANativeWindow is managed by this image reader - ..Default::default() - }; - let status = unsafe { AImageReader_getWindow(self.handle, &mut result.handle) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result) - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_setimagelistener - pub(crate) fn _set_image_listener(&mut self) -> MediaResult { - Ok(()) // TODO - } - - /// See: https://developer.android.com/ndk/reference/group/media#aimagereader_acquirelatestimage - pub fn acquire_latest_image(&self) -> MediaResult { - let mut result = Image::default(); - let status = unsafe { AImageReader_acquireLatestImage(self.handle, &mut result.handle) }; - if status != media_status_t::AMEDIA_OK { - return Err(status.into()); - } - Ok(result) - } -} diff --git a/src/shared/drivers/android/media_status.rs b/src/shared/drivers/android/media_status.rs deleted file mode 100644 index fed481d..0000000 --- a/src/shared/drivers/android/media_status.rs +++ /dev/null @@ -1,26 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use derive_more::Display; -use ndk_sys::media_status_t; - -pub type MediaResult = core::result::Result; - -#[derive(Clone, Copy, Debug, Display)] -#[display("{}", _0.0)] -#[allow(unused)] -pub struct MediaStatus(pub(crate) media_status_t); - -impl core::error::Error for MediaStatus {} - -impl Default for MediaStatus { - fn default() -> Self { - MediaStatus(media_status_t::AMEDIA_OK) - } -} - -impl From for MediaStatus { - fn from(input: media_status_t) -> Self { - assert!(input != media_status_t::AMEDIA_ERROR_INVALID_PARAMETER); - Self(input) - } -} diff --git a/src/shared/drivers/android/native_window.rs b/src/shared/drivers/android/native_window.rs deleted file mode 100644 index 624a611..0000000 --- a/src/shared/drivers/android/native_window.rs +++ /dev/null @@ -1,27 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use core::ptr::null_mut; -use ndk_sys::{ANativeWindow, ANativeWindow_acquire, ANativeWindow_release}; - -#[derive(Debug, Default)] -pub struct NativeWindow { - pub(crate) handle: *mut ANativeWindow, - pub(crate) owned: bool, -} - -impl Drop for NativeWindow { - fn drop(&mut self) { - if self.owned { - unsafe { - ANativeWindow_release(self.handle); - self.handle = null_mut(); - } - } - } -} - -impl NativeWindow { - pub fn acquire(&self) { - unsafe { ANativeWindow_acquire(self.handle) } - } -} diff --git a/src/shared/drivers/avf.rs b/src/shared/drivers/avf.rs deleted file mode 100644 index e6203bc..0000000 --- a/src/shared/drivers/avf.rs +++ /dev/null @@ -1,65 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::{ - CameraBackend, CameraConfig, CameraDriver, CameraError, CameraEvent, FrameMsg, -}; -use alloc::borrow::Cow; -use std::{any::Any, sync::mpsc::SyncSender}; - -#[derive(Debug)] -pub struct AvfCameraDriver { - _config: CameraConfig, - _frame_tx: SyncSender, - _events_tx: SyncSender, -} - -impl dogma::Named for AvfCameraDriver { - fn name(&self) -> Cow<'_, str> { - "avf".into() - } -} - -impl AvfCameraDriver { - pub fn open( - _input_url: impl AsRef, - config: CameraConfig, - frame_tx: SyncSender, - events_tx: SyncSender, - ) -> Result { - Ok(Self { - _config: config, - _frame_tx: frame_tx, - _events_tx: events_tx, - }) - } -} - -impl CameraDriver for AvfCameraDriver { - fn backend(&self) -> CameraBackend { - CameraBackend::Avf - } - - fn start(&mut self) -> Result<(), CameraError> { - Err(CameraError::unsupported( - "avfoundation backend not implemented", - )) - } - - fn stop(&mut self) -> Result<(), CameraError> { - Ok(()) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -impl Drop for AvfCameraDriver { - fn drop(&mut self) { - let _ = self.stop(); - } -} diff --git a/src/shared/drivers/dshow.rs b/src/shared/drivers/dshow.rs deleted file mode 100644 index 3f12bfd..0000000 --- a/src/shared/drivers/dshow.rs +++ /dev/null @@ -1,52 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::{ - CameraBackend, CameraConfig, CameraDriver, CameraError, CameraEvent, FrameMsg, -}; -use std::{any::Any, sync::mpsc::SyncSender}; - -#[derive(Debug)] -pub struct DshowCameraDriver { - _config: CameraConfig, - _frame_tx: SyncSender, - _events_tx: SyncSender, -} - -impl DshowCameraDriver { - pub fn open( - _input_url: impl AsRef, - config: CameraConfig, - frame_tx: SyncSender, - events_tx: SyncSender, - ) -> Result { - Ok(Self { - _config: config, - _frame_tx: frame_tx, - _events_tx: events_tx, - }) - } -} - -impl CameraDriver for DshowCameraDriver { - fn backend(&self) -> CameraBackend { - CameraBackend::Dshow - } - - fn start(&mut self) -> Result<(), CameraError> { - Err(CameraError::unsupported( - "directshow backend not implemented", - )) - } - - fn stop(&mut self) -> Result<(), CameraError> { - Ok(()) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} diff --git a/src/shared/drivers/ffmpeg.rs b/src/shared/drivers/ffmpeg.rs deleted file mode 100644 index 298489a..0000000 --- a/src/shared/drivers/ffmpeg.rs +++ /dev/null @@ -1,350 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::{ - CameraBackend, CameraConfig, CameraDriver, CameraError, CameraEvent, Frame, FrameMsg, - try_send_frame, -}; -use bytes::Bytes; -use std::{ - any::Any, - env, - io::Read, - process::{Child, Command, ExitStatus, Stdio}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - mpsc::SyncSender, - }, - thread::JoinHandle, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -pub struct FfmpegCameraDriver { - config: CameraConfig, - child: Option>>, - stop: Arc, - reader_join: Option>, - monitor_join: Option>, - frame_tx: SyncSender, - events_tx: SyncSender, -} - -impl core::fmt::Debug for FfmpegCameraDriver { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("FfmpegCameraDriver") - .field("config", &self.config) - .field("child", &self.child.as_ref().map(|_| "")) - .finish() - } -} - -impl FfmpegCameraDriver { - pub fn open( - _input_url: impl AsRef, - config: CameraConfig, - frame_tx: SyncSender, - events_tx: SyncSender, - ) -> Result { - Ok(Self { - config, - child: None, - stop: Arc::new(AtomicBool::new(false)), - reader_join: None, - monitor_join: None, - frame_tx, - events_tx, - }) - } - - #[inline] - fn now_ns_best_effort() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0) - } - - fn spawn(&self) -> Result { - spawn_reader(&self.config) - } - - fn stop_child(&mut self) { - let Some(child_arc) = self.child.take() else { - return; - }; - if let Ok(mut g) = child_arc.lock() { - terminate_child(&mut *g); - } - } -} - -impl CameraDriver for FfmpegCameraDriver { - fn backend(&self) -> CameraBackend { - CameraBackend::Ffmpeg - } - - fn start(&mut self) -> Result<(), CameraError> { - if self.child.is_some() { - return Ok(()); - } - - self.stop.store(false, Ordering::Relaxed); - - let mut child = self.spawn()?; - let stdout = child - .stdout - .take() - .ok_or_else(|| CameraError::other("ffmpeg stdout not piped"))?; - - let width = self.config.width; - let height = self.config.height; - let stride = width.saturating_mul(3); - let frame_size = (stride as usize).saturating_mul(height as usize); - - let child_arc = Arc::new(Mutex::new(child)); - self.child = Some(Arc::clone(&child_arc)); - - let stop = Arc::clone(&self.stop); - let frame_tx = self.frame_tx.clone(); - let events_tx = self.events_tx.clone(); - - let reader_join = std::thread::spawn(move || { - let mut reader = std::io::BufReader::new(stdout); - let mut buf = vec![0u8; frame_size]; - - while !stop.load(Ordering::Relaxed) { - match reader.read_exact(&mut buf) { - Ok(()) => { - let ts = FfmpegCameraDriver::now_ns_best_effort(); - let frame = - Frame::new_rgb8(Bytes::copy_from_slice(&buf), width, height, stride) - .with_timestamp_ns(ts); - try_send_frame(&frame_tx, &events_tx, CameraBackend::Ffmpeg, frame); - }, - Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { - let _ = events_tx.try_send(CameraEvent::Error { - backend: CameraBackend::Ffmpeg, - error: CameraError::other("ffmpeg stream ended (EOF)"), - }); - break; - }, - Err(e) => { - let _ = events_tx.try_send(CameraEvent::Error { - backend: CameraBackend::Ffmpeg, - error: CameraError::driver("ffmpeg read", e), - }); - break; - }, - } - } - }); - - let stop2 = Arc::clone(&self.stop); - let events_tx2 = self.events_tx.clone(); - let child_arc2 = Arc::clone(&child_arc); - - let monitor_join = std::thread::spawn(move || { - while !stop2.load(Ordering::Relaxed) { - let status = { - let mut g = match child_arc2.lock() { - Ok(v) => v, - Err(p) => p.into_inner(), - }; - g.try_wait() - }; - - match status { - Ok(Some(s)) => { - // If we are stopping intentionally, don't spam as "error". - if stop2.load(Ordering::Relaxed) { - break; - } - let _ = events_tx2.try_send(CameraEvent::Error { - backend: CameraBackend::Ffmpeg, - error: CameraError::other(format!("ffmpeg exited: {}", format_exit(s))), - }); - break; - }, - Ok(None) => std::thread::sleep(Duration::from_millis(150)), - Err(e) => { - if stop2.load(Ordering::Relaxed) { - break; - } - let _ = events_tx2.try_send(CameraEvent::Error { - backend: CameraBackend::Ffmpeg, - error: CameraError::driver("ffmpeg wait", e), - }); - break; - }, - } - } - }); - - self.reader_join = Some(reader_join); - self.monitor_join = Some(monitor_join); - - Ok(()) - } - - fn stop(&mut self) -> Result<(), CameraError> { - self.stop.store(true, Ordering::Relaxed); - self.stop_child(); - - if let Some(j) = self.reader_join.take() { - let _ = j.join(); - } - if let Some(j) = self.monitor_join.take() { - let _ = j.join(); - } - - Ok(()) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -impl Drop for FfmpegCameraDriver { - fn drop(&mut self) { - let _ = self.stop(); - } -} - -fn spawn_reader(config: &CameraConfig) -> Result { - let device = config.device.as_deref().unwrap_or("").trim(); - let input_device = get_input_device(device); - - // On macOS/AVFoundation, many devices reject "odd" framerates even when listed. - // For a stable CLI, keep capture at a safe default and let the reader throttle output. - #[cfg(target_os = "macos")] - let input_fps: f64 = 30.0; - - #[cfg(not(target_os = "macos"))] - let input_fps: f64 = { - let fps = if config.fps.is_finite() && config.fps > 0.1 { - config.fps - } else { - 30.0 - }; - fps.min(240.0) - }; - - let mut ffargs: Vec = vec![ - "-hide_banner".into(), - "-nostdin".into(), - "-nostats".into(), - "-f".into(), - ffmpeg_format().into(), - "-loglevel".into(), - "error".into(), - "-video_size".into(), - format!("{}x{}", config.width, config.height), - "-framerate".into(), - format!("{input_fps}"), - ]; - - #[cfg(target_os = "macos")] - { - ffargs.push("-pixel_format".into()); - ffargs.push("0rgb".into()); - } - - ffargs.extend([ - "-i".into(), - input_device, - "-pix_fmt".into(), - "rgb24".into(), - "-f".into(), - "rawvideo".into(), - "pipe:1".into(), - ]); - - let stderr = if config.diagnostics || env::var_os("ASIMOV_CAMERA_FFMPEG_STDERR").is_some() { - Stdio::inherit() - } else { - Stdio::null() - }; - - Command::new("ffmpeg") - .args(&ffargs) - .stdout(Stdio::piped()) - .stderr(stderr) - .spawn() - .map_err(|e| CameraError::driver("spawning ffmpeg", e)) -} - -fn format_exit(status: ExitStatus) -> String { - if let Some(code) = status.code() { - format!("code={code}") - } else { - "terminated".to_string() - } -} - -fn terminate_child(child: &mut Child) { - #[cfg(unix)] - { - unsafe { - let _ = libc::kill(child.id() as i32, libc::SIGTERM); - } - let start = std::time::Instant::now(); - while start.elapsed() < Duration::from_millis(900) { - if let Ok(Some(_)) = child.try_wait() { - return; - } - std::thread::sleep(Duration::from_millis(20)); - } - let _ = child.kill(); - let _ = child.wait(); - } - #[cfg(windows)] - { - let _ = child.kill(); - let _ = child.wait(); - } - #[cfg(not(any(unix, windows)))] - { - let _ = child.kill(); - let _ = child.wait(); - } -} - -#[cfg(target_os = "macos")] -fn ffmpeg_format() -> &'static str { - "avfoundation" -} - -#[cfg(target_os = "linux")] -fn ffmpeg_format() -> &'static str { - "v4l2" -} - -#[cfg(target_os = "windows")] -fn ffmpeg_format() -> &'static str { - "dshow" -} - -#[cfg(target_os = "macos")] -fn get_input_device(device: &str) -> String { - device.strip_prefix("avf:").unwrap_or(device).to_string() -} - -#[cfg(target_os = "linux")] -fn get_input_device(device: &str) -> String { - let d = device.strip_prefix("file:").unwrap_or(device); - if d.chars().all(|c| c.is_ascii_digit()) { - format!("/dev/video{d}") - } else { - d.to_string() - } -} - -#[cfg(target_os = "windows")] -fn get_input_device(device: &str) -> String { - device.strip_prefix("dshow:").unwrap_or(device).to_string() -} diff --git a/src/shared/drivers/v4l2.rs b/src/shared/drivers/v4l2.rs deleted file mode 100644 index 23398a9..0000000 --- a/src/shared/drivers/v4l2.rs +++ /dev/null @@ -1,50 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use crate::shared::{ - CameraBackend, CameraConfig, CameraDriver, CameraError, CameraEvent, FrameMsg, -}; -use std::{any::Any, sync::mpsc::SyncSender}; - -#[derive(Debug)] -pub struct V4l2CameraDriver { - _config: CameraConfig, - _frame_tx: SyncSender, - _events_tx: SyncSender, -} - -impl V4l2CameraDriver { - pub fn open( - _input_url: impl AsRef, - config: CameraConfig, - frame_tx: SyncSender, - events_tx: SyncSender, - ) -> Result { - Ok(Self { - _config: config, - _frame_tx: frame_tx, - _events_tx: events_tx, - }) - } -} - -impl CameraDriver for V4l2CameraDriver { - fn backend(&self) -> CameraBackend { - CameraBackend::V4l2 - } - - fn start(&mut self) -> Result<(), CameraError> { - Err(CameraError::unsupported("v4l2 backend not implemented")) - } - - fn stop(&mut self) -> Result<(), CameraError> { - Ok(()) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} diff --git a/src/shared/error.rs b/src/shared/error.rs deleted file mode 100644 index 60b79e6..0000000 --- a/src/shared/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use std::error::Error as StdError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum CameraError { - #[error("no suitable camera backend available")] - NoDriver, - - #[error("driver not applicable for this target/configuration")] - NotApplicable, - - #[error("no camera device available")] - NoCamera, - - #[error("driver is not configured")] - NotConfigured, - - #[error("unsupported: {0}")] - Unsupported(String), - - #[error("invalid configuration: {0}")] - InvalidConfig(String), - - #[error("stream closed")] - Closed, - - #[error("driver error while {context}")] - DriverError { - context: &'static str, - #[source] - source: Box, - }, - - #[error("{0}")] - Other(String), -} - -impl CameraError { - #[inline] - pub fn driver(context: &'static str, source: impl StdError + Send + Sync + 'static) -> Self { - Self::DriverError { - context, - source: Box::new(source), - } - } - - #[inline] - pub fn unsupported(msg: impl Into) -> Self { - Self::Unsupported(msg.into()) - } - - #[inline] - pub fn invalid_config(msg: impl Into) -> Self { - Self::InvalidConfig(msg.into()) - } - - #[inline] - pub fn other(msg: impl Into) -> Self { - Self::Other(msg.into()) - } -} diff --git a/src/shared/frame.rs b/src/shared/frame.rs deleted file mode 100644 index a04e39a..0000000 --- a/src/shared/frame.rs +++ /dev/null @@ -1,78 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use bytes::Bytes; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum PixelFormat { - Rgb8, - Bgra8, -} - -impl PixelFormat { - #[inline] - pub const fn bytes_per_pixel(self) -> u32 { - match self { - PixelFormat::Rgb8 => 3, - PixelFormat::Bgra8 => 4, - } - } -} - -#[derive(Clone, Debug)] -pub struct Frame { - pub data: Bytes, - pub width: u32, - pub height: u32, - pub stride: u32, - pub pixel_format: PixelFormat, - pub timestamp_ns: u64, -} - -impl Frame { - #[inline] - pub fn new( - data: Bytes, - width: u32, - height: u32, - stride: u32, - pixel_format: PixelFormat, - ) -> Self { - Self { - data, - width, - height, - stride, - pixel_format, - timestamp_ns: 0, - } - } - - #[inline] - pub fn new_rgb8(data: Bytes, width: u32, height: u32, stride: u32) -> Self { - Self::new(data, width, height, stride, PixelFormat::Rgb8) - } - - #[inline] - pub fn new_bgra8(data: Bytes, width: u32, height: u32, stride: u32) -> Self { - Self::new(data, width, height, stride, PixelFormat::Bgra8) - } - - #[inline] - pub fn with_timestamp_ns(mut self, timestamp_ns: u64) -> Self { - self.timestamp_ns = timestamp_ns; - self - } - - #[inline] - pub fn validate(&self) -> bool { - let bpp = self.pixel_format.bytes_per_pixel(); - if self.width == 0 || self.height == 0 || self.stride == 0 { - return false; - } - if self.stride < self.width.saturating_mul(bpp) { - return false; - } - let min_len = (self.stride as usize).saturating_mul(self.height as usize); - self.data.len() >= min_len - } -} diff --git a/src/shared/mod.rs b/src/shared/mod.rs deleted file mode 100644 index 7f07ae3..0000000 --- a/src/shared/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -mod config; -pub use config::*; - -mod driver; -pub use driver::*; - -pub mod drivers { - /// Camera driver using FFmpeg. - #[cfg(all( - feature = "ffmpeg", - any(target_os = "macos", target_os = "linux", target_os = "windows") - ))] - pub mod ffmpeg; - - /// Camera driver using the NDK on Android. - #[cfg(all(feature = "android", target_os = "android"))] - pub mod android; - - /// Camera driver using AVFoundation on iOS and macOS. - #[cfg(all(feature = "avf", any(target_os = "ios", target_os = "macos")))] - pub mod avf; - - /// Camera driver using DShow on Windows. - #[cfg(all(feature = "dshow", target_os = "windows"))] - pub mod dshow; - - /// Camera driver using V4L2 on Linux. - #[cfg(all(feature = "v4l2", target_os = "linux"))] - pub mod v4l2; -} - -mod error; -pub use error::*; - -mod open; -pub use open::*; - -mod frame; -pub use frame::*; diff --git a/src/shared/open.rs b/src/shared/open.rs deleted file mode 100644 index 3bc7a82..0000000 --- a/src/shared/open.rs +++ /dev/null @@ -1,46 +0,0 @@ -// This is free and unencumbered software released into the public domain. - -use super::{Camera, CameraConfig, CameraError}; - -#[allow(unused_imports)] -use super::{CameraBackend, CameraEvent, Dispatcher}; -#[allow(unused_imports)] -use std::sync::mpsc::sync_channel; - -pub fn open_camera( - input_url: impl AsRef, - config: CameraConfig, -) -> Result { - // Defining the macro inside the function limits its scope - // and helps suppress "unused" warnings when no features are enabled. - #[allow(unused_macros)] - macro_rules! init_camera { - ($driver_type:ty, $backend:expr, $url:expr, $config:expr) => {{ - let (events_tx, events_rx) = sync_channel::(128); - let dispatcher = Dispatcher::new($config.buffer_frames, $backend, events_tx.clone()); - let frame_tx = dispatcher.sender(); - - let driver = - <$driver_type>::open($url.as_ref().to_string(), $config, frame_tx, events_tx)?; - - Ok(Camera::new(Box::new(driver), dispatcher, events_rx)) - }}; - } - - cfg_if::cfg_if! { - if #[cfg(all(feature = "android", target_os = "android"))] { - init_camera!(super::drivers::android::AndroidCameraDriver, CameraBackend::Android, input_url, config) - } else if #[cfg(all(feature = "ffmpeg", any(target_os = "macos", target_os = "linux", target_os = "windows")))] { - init_camera!(super::drivers::ffmpeg::FfmpegCameraDriver, CameraBackend::Ffmpeg, input_url, config) - } else if #[cfg(all(feature = "avf", any(target_os = "ios", target_os = "macos")))] { - init_camera!(super::drivers::avf::AvfCameraDriver, CameraBackend::Avf, input_url, config) - } else if #[cfg(all(feature = "dshow", target_os = "windows"))] { - init_camera!(super::drivers::dshow::DshowCameraDriver, CameraBackend::Dshow, input_url, config) - } else if #[cfg(all(feature = "v4l2", target_os = "linux"))] { - init_camera!(super::drivers::v4l2::V4l2CameraDriver, CameraBackend::V4l2, input_url, config) - } else { - let _ = (input_url, config); - Err(CameraError::NoDriver) - } - } -}