refactor: 改用FFmpeg枚举DirectShow设备并推流到MediaMTX,移除Nokhwa依赖
This commit is contained in:
349
capture-card-viewer/Cargo.lock
generated
349
capture-card-viewer/Cargo.lock
generated
@@ -77,12 +77,6 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -158,12 +152,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -180,7 +168,7 @@ dependencies = [
|
||||
"log",
|
||||
"nix 0.25.1",
|
||||
"slotmap",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
@@ -190,7 +178,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"nokhwa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -379,12 +368,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.24.1"
|
||||
@@ -408,7 +391,7 @@ dependencies = [
|
||||
"glow",
|
||||
"glutin",
|
||||
"glutin-winit",
|
||||
"image 0.24.9",
|
||||
"image",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc",
|
||||
@@ -416,7 +399,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"static_assertions",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -539,18 +522,6 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -581,12 +552,6 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
@@ -615,19 +580,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -858,18 +810,6 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -892,6 +832,12 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -903,7 +849,7 @@ dependencies = [
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
@@ -942,7 +888,7 @@ version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1099,60 +1045,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mozjpeg"
|
||||
version = "0.10.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"libc",
|
||||
"mozjpeg-sys",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mozjpeg-sys"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dunce",
|
||||
"libc",
|
||||
"nasm-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanorand"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nasm-rs"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.7.0"
|
||||
@@ -1164,7 +1056,7 @@ dependencies = [
|
||||
"ndk-sys",
|
||||
"num_enum 0.5.11",
|
||||
"raw-window-handle",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1213,43 +1105,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nokhwa"
|
||||
version = "0.10.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4cae50786bfa1214ed441f98addbea51ca1b9aaa9e4bf5369cda36654b3efaa"
|
||||
dependencies = [
|
||||
"flume",
|
||||
"image 0.25.10",
|
||||
"nokhwa-bindings-windows",
|
||||
"nokhwa-core",
|
||||
"paste",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nokhwa-bindings-windows"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899799275c93ef69bbe8cb888cf6f8249abe751cbc50be5299105022aec14a1c"
|
||||
dependencies = [
|
||||
"nokhwa-core",
|
||||
"once_cell",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nokhwa-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "109975552bbd690894f613bce3d408222911e317197c72b2e8b9a1912dc261ae"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"image 0.25.10",
|
||||
"mozjpeg",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1457,12 +1312,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1528,12 +1377,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -1582,15 +1425,6 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -1638,6 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1660,6 +1495,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1722,15 +1570,6 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -1782,16 +1621,7 @@ version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1805,17 +1635,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -2118,107 +1937,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -2302,15 +2026,6 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -2634,3 +2349,9 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -4,9 +4,10 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nokhwa = { version = "0.10", features = ["input-msmf"] }
|
||||
eframe = { version = "0.24", default-features = false, features = ["default_fonts", "glow"] }
|
||||
egui = "0.24"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
winapi = { version = "0.3", features = ["winuser", "windef", "processthreadsapi", "handleapi", "winbase"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,114 +1,300 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use eframe::{egui, App, Frame, NativeOptions};
|
||||
use egui::{Color32, FontFamily, FontId, RichText, TextureHandle, TextureOptions};
|
||||
use nokhwa::utils::{ApiBackend, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa::Camera;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use egui::{Color32, FontFamily, FontId, RichText};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
struct CaptureState {
|
||||
device_name: Arc<Mutex<String>>,
|
||||
frame_data: Arc<Mutex<Option<(usize, usize, Vec<u8>)>>>,
|
||||
running: Arc<Mutex<bool>>,
|
||||
error: Arc<Mutex<Option<String>>>,
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Config {
|
||||
ffmpeg_path: String,
|
||||
server_ip: String,
|
||||
stream_path: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ffmpeg_path: r"D:\ScreenCast\ffmpeg\bin\ffmpeg.exe".to_string(),
|
||||
server_ip: "192.168.1.100".to_string(),
|
||||
stream_path: "hdmi".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn load() -> Self {
|
||||
if let Ok(content) = fs::read_to_string("capture_card_config.json") {
|
||||
if let Ok(config) = serde_json::from_str(&content) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
if let Ok(json) = serde_json::to_string_pretty(self) {
|
||||
let _ = fs::write("capture_card_config.json", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Message {
|
||||
DevicesFound(Vec<String>),
|
||||
ScanFailed(String),
|
||||
StatusUpdate { ffmpeg_ok: bool, server_ok: bool },
|
||||
}
|
||||
|
||||
struct CaptureCardViewer {
|
||||
state: CaptureState,
|
||||
texture: Option<TextureHandle>,
|
||||
config: Config,
|
||||
temp_config: Config,
|
||||
show_settings: bool,
|
||||
|
||||
video_devices: Vec<String>,
|
||||
selected_device_idx: usize,
|
||||
is_scanning: bool,
|
||||
|
||||
is_streaming: bool,
|
||||
ffmpeg_ok: bool,
|
||||
server_ok: bool,
|
||||
|
||||
log_message: String,
|
||||
|
||||
rx: Receiver<Message>,
|
||||
tx: Sender<Message>,
|
||||
}
|
||||
|
||||
impl CaptureCardViewer {
|
||||
fn new() -> Self {
|
||||
let state = CaptureState {
|
||||
device_name: Arc::new(Mutex::new(String::new())),
|
||||
frame_data: Arc::new(Mutex::new(None)),
|
||||
running: Arc::new(Mutex::new(true)),
|
||||
error: Arc::new(Mutex::new(None)),
|
||||
let config = Config::load();
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let ffmpeg_ok = Path::new(&config.ffmpeg_path).exists();
|
||||
let server_ok = check_server(&config.server_ip, 8554);
|
||||
|
||||
let mut app = Self {
|
||||
temp_config: config.clone(),
|
||||
config,
|
||||
show_settings: false,
|
||||
video_devices: Vec::new(),
|
||||
selected_device_idx: 0,
|
||||
is_scanning: false,
|
||||
is_streaming: false,
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
log_message: "就绪,请点击「扫描设备」查找视频采集卡".to_string(),
|
||||
rx,
|
||||
tx,
|
||||
};
|
||||
|
||||
let capture_state = CaptureState {
|
||||
device_name: state.device_name.clone(),
|
||||
frame_data: state.frame_data.clone(),
|
||||
running: state.running.clone(),
|
||||
error: state.error.clone(),
|
||||
};
|
||||
app.scan_devices();
|
||||
app.start_status_thread();
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn scan_devices(&mut self) {
|
||||
if self.is_scanning {
|
||||
return;
|
||||
}
|
||||
self.is_scanning = true;
|
||||
self.log_message = "正在扫描 DirectShow 视频设备...".to_string();
|
||||
|
||||
let ffmpeg_path = self.config.ffmpeg_path.clone();
|
||||
let tx = self.tx.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
capture_thread(capture_state);
|
||||
});
|
||||
|
||||
CaptureCardViewer {
|
||||
state,
|
||||
texture: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_thread(state: CaptureState) {
|
||||
let devices = match nokhwa::query(ApiBackend::MediaFoundation) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("查询设备失败: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let result = enumerate_dshow_devices(&ffmpeg_path);
|
||||
match result {
|
||||
Ok(devices) => {
|
||||
if devices.is_empty() {
|
||||
*state.error.lock().unwrap() = Some("未找到视频采集设备".to_string());
|
||||
let _ = tx.send(Message::ScanFailed(
|
||||
"未找到任何视频采集设备,请确认设备已连接".to_string(),
|
||||
));
|
||||
} else {
|
||||
let _ = tx.send(Message::DevicesFound(devices));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(Message::ScanFailed(format!("扫描失败: {}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn start_status_thread(&self) {
|
||||
let tx = self.tx.clone();
|
||||
let ffmpeg_path = self.config.ffmpeg_path.clone();
|
||||
let server_ip = self.config.server_ip.clone();
|
||||
|
||||
thread::spawn(move || loop {
|
||||
let ffmpeg_ok = Path::new(&ffmpeg_path).exists();
|
||||
let server_ok = check_server(&server_ip, 8554);
|
||||
let _ = tx.send(Message::StatusUpdate {
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
});
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
});
|
||||
}
|
||||
|
||||
fn start_stream(&mut self) {
|
||||
if self.is_streaming {
|
||||
return;
|
||||
}
|
||||
if !self.ffmpeg_ok {
|
||||
self.log_message = "错误: FFmpeg 未找到,请检查配置".to_string();
|
||||
return;
|
||||
}
|
||||
if self.video_devices.is_empty() {
|
||||
self.log_message = "错误: 没有可用的视频设备,请先扫描设备".to_string();
|
||||
return;
|
||||
}
|
||||
|
||||
let device_info = &devices[0];
|
||||
*state.device_name.lock().unwrap() = device_info.human_name().to_string();
|
||||
|
||||
let index = device_info.index().clone();
|
||||
let requested = RequestedFormat::new::<nokhwa::pixel_format::RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate,
|
||||
let device_name = self.video_devices[self.selected_device_idx].clone();
|
||||
let video_input = format!("video={}", device_name);
|
||||
let output_url = format!(
|
||||
"rtsp://{}:8554/{}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
);
|
||||
|
||||
let mut camera = match Camera::new(index, requested) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("打开摄像头失败: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let args = vec![
|
||||
"-f",
|
||||
"dshow",
|
||||
"-i",
|
||||
&video_input,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-f",
|
||||
"rtsp",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
&output_url,
|
||||
];
|
||||
|
||||
if let Err(e) = camera.open_stream() {
|
||||
*state.error.lock().unwrap() = Some(format!("启动视频流失败: {}", e));
|
||||
return;
|
||||
match Command::new(&self.config.ffmpeg_path)
|
||||
.args(&args)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {
|
||||
self.is_streaming = true;
|
||||
self.log_message = format!(
|
||||
"推流已启动\n设备: {}\n地址: {}",
|
||||
device_name, output_url
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
self.log_message = format!("启动 FFmpeg 失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while *state.running.lock().unwrap() {
|
||||
match camera.frame() {
|
||||
Ok(buffer) => {
|
||||
match buffer.decode_image::<nokhwa::pixel_format::RgbFormat>() {
|
||||
Ok(rgb_image) => {
|
||||
let w = rgb_image.width() as usize;
|
||||
let h = rgb_image.height() as usize;
|
||||
let raw = rgb_image.into_raw();
|
||||
let mut rgba = Vec::with_capacity(w * h * 4);
|
||||
for chunk in raw.chunks(3) {
|
||||
rgba.push(chunk[0]);
|
||||
rgba.push(chunk[1]);
|
||||
rgba.push(chunk[2]);
|
||||
rgba.push(255);
|
||||
fn stop_stream(&mut self) {
|
||||
let _ = Command::new("taskkill")
|
||||
.args(&["/F", "/IM", "ffmpeg.exe"])
|
||||
.output();
|
||||
self.is_streaming = false;
|
||||
self.log_message = "推流已停止".to_string();
|
||||
}
|
||||
*state.frame_data.lock().unwrap() = Some((w, h, rgba));
|
||||
*state.error.lock().unwrap() = None;
|
||||
}
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("解码帧失败: {}", e));
|
||||
|
||||
fn enumerate_dshow_devices(ffmpeg_path: &str) -> Result<Vec<String>, String> {
|
||||
if !Path::new(ffmpeg_path).exists() {
|
||||
return Err(format!("FFmpeg 不存在: {}", ffmpeg_path));
|
||||
}
|
||||
|
||||
let output = Command::new(ffmpeg_path)
|
||||
.args(&["-list_devices", "true", "-f", "dshow", "-i", "dummy"])
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| format!("执行 FFmpeg 失败: {}", e))?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let mut devices = Vec::new();
|
||||
let mut is_video = false;
|
||||
|
||||
for line in stderr.lines() {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.contains("(video)") {
|
||||
is_video = true;
|
||||
} else if trimmed.contains("(audio)") {
|
||||
is_video = false;
|
||||
}
|
||||
|
||||
if is_video {
|
||||
if let Some(name) = extract_device_name(trimmed) {
|
||||
if !devices.contains(&name) {
|
||||
devices.push(name);
|
||||
}
|
||||
is_video = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("捕获帧失败: {}", e));
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn extract_device_name(line: &str) -> Option<String> {
|
||||
if let Some(start) = line.find('"') {
|
||||
if let Some(end) = line[start + 1..].find('"') {
|
||||
let name = line[start + 1..start + 1 + end].to_string();
|
||||
if !name.is_empty() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn check_server(ip: &str, port: u16) -> bool {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
addr.parse::<std::net::SocketAddr>().is_ok()
|
||||
&& TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(2)).is_ok()
|
||||
}
|
||||
|
||||
fn configure_fonts(ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
let font_paths = [
|
||||
r"C:\Windows\Fonts\msyh.ttc",
|
||||
r"C:\Windows\Fonts\simhei.ttf",
|
||||
r"C:\Windows\Fonts\simsun.ttc",
|
||||
];
|
||||
for font_path in &font_paths {
|
||||
if let Ok(font_data) = std::fs::read(font_path) {
|
||||
let font_name = Path::new(font_path)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("chinese_font");
|
||||
fonts.font_data.insert(
|
||||
font_name.to_owned(),
|
||||
egui::FontData::from_owned(font_data),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&FontFamily::Proportional)
|
||||
.unwrap()
|
||||
.insert(0, font_name.to_owned());
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&FontFamily::Monospace)
|
||||
.unwrap()
|
||||
.push(font_name.to_owned());
|
||||
ctx.set_fonts(fonts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,116 +303,280 @@ impl App for CaptureCardViewer {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
|
||||
static FONT_LOADED: std::sync::Once = std::sync::Once::new();
|
||||
FONT_LOADED.call_once(|| {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
let font_paths = [
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc",
|
||||
];
|
||||
for font_path in &font_paths {
|
||||
if let Ok(font_data) = std::fs::read(font_path) {
|
||||
fonts.font_data.insert(
|
||||
"chinese_font".to_owned(),
|
||||
egui::FontData::from_owned(font_data),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Proportional)
|
||||
.unwrap()
|
||||
.insert(0, "chinese_font".to_owned());
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Monospace)
|
||||
.unwrap()
|
||||
.push("chinese_font".to_owned());
|
||||
ctx.set_fonts(fonts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
configure_fonts(ctx);
|
||||
});
|
||||
|
||||
let frame_update = {
|
||||
let frame_guard = self.state.frame_data.lock().unwrap();
|
||||
frame_guard.as_ref().map(|(w, h, rgba)| {
|
||||
if *w > 0 && *h > 0 {
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied([*w, *h], rgba);
|
||||
Some(color_image)
|
||||
while let Ok(msg) = self.rx.try_recv() {
|
||||
match msg {
|
||||
Message::DevicesFound(devices) => {
|
||||
self.video_devices = devices;
|
||||
self.selected_device_idx = 0;
|
||||
self.is_scanning = false;
|
||||
if self.video_devices.is_empty() {
|
||||
self.log_message = "未找到视频采集设备".to_string();
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(Some(color_image)) = frame_update {
|
||||
if let Some(tex) = self.texture.as_mut() {
|
||||
tex.set(color_image, TextureOptions::LINEAR);
|
||||
} else {
|
||||
self.texture = Some(
|
||||
ctx.load_texture("capture_frame", color_image, TextureOptions::LINEAR),
|
||||
self.log_message = format!(
|
||||
"找到 {} 个视频设备:\n{}",
|
||||
self.video_devices.len(),
|
||||
self.video_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| format!(" {}. {}", i + 1, d))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::ScanFailed(e) => {
|
||||
self.is_scanning = false;
|
||||
self.video_devices.clear();
|
||||
self.log_message = e;
|
||||
}
|
||||
Message::StatusUpdate {
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
} => {
|
||||
self.ffmpeg_ok = ffmpeg_ok;
|
||||
self.server_ok = server_ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(10.0);
|
||||
|
||||
let device_name = self.state.device_name.lock().unwrap().clone();
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(if device_name.is_empty() {
|
||||
"正在查找设备..."
|
||||
} else {
|
||||
&device_name
|
||||
})
|
||||
.font(FontId::new(18.0, FontFamily::Proportional))
|
||||
RichText::new("视频采集卡推流工具")
|
||||
.font(FontId::new(20.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
let error = self.state.error.lock().unwrap();
|
||||
if let Some(e) = error.as_ref() {
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new(e)
|
||||
.font(FontId::new(12.0, FontFamily::Proportional))
|
||||
.color(Color32::RED),
|
||||
);
|
||||
}
|
||||
drop(error);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if let Some(tex) = &self.texture {
|
||||
let avail_width = ui.available_width();
|
||||
let size = tex.size_vec2();
|
||||
let scale = (avail_width / size.x).min(1.0);
|
||||
let display_size = size * scale;
|
||||
ui.add(egui::Image::new(tex).max_size(display_size));
|
||||
} else {
|
||||
ui.add_space(100.0);
|
||||
ui.label(
|
||||
RichText::new("等待视频画面...")
|
||||
RichText::new("状态检查")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.color(Color32::from_rgb(128, 128, 128)),
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let (color, text) = if self.ffmpeg_ok {
|
||||
(Color32::GREEN, "FFmpeg: 已就绪")
|
||||
} else {
|
||||
(Color32::RED, "FFmpeg: 未找到")
|
||||
};
|
||||
ui.colored_label(color, "●");
|
||||
ui.label(text);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let (color, text) = if self.server_ok {
|
||||
(Color32::GREEN, "MediaMTX 服务器: 已连接")
|
||||
} else {
|
||||
(Color32::from_rgb(255, 165, 0), "MediaMTX 服务器: 未连接")
|
||||
};
|
||||
ui.colored_label(color, "●");
|
||||
ui.label(text);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("设备选择")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let scan_text = if self.is_scanning {
|
||||
"扫描中..."
|
||||
} else {
|
||||
"扫描设备"
|
||||
};
|
||||
if ui
|
||||
.add_sized(
|
||||
[100.0, 28.0],
|
||||
egui::Button::new(scan_text),
|
||||
)
|
||||
.clicked()
|
||||
&& !self.is_scanning
|
||||
{
|
||||
self.scan_devices();
|
||||
}
|
||||
|
||||
if self.video_devices.is_empty() {
|
||||
ui.label(
|
||||
RichText::new("未检测到设备").color(Color32::from_rgb(180, 180, 180)),
|
||||
);
|
||||
} else {
|
||||
let selected_text = if self.selected_device_idx < self.video_devices.len() {
|
||||
&self.video_devices[self.selected_device_idx]
|
||||
} else {
|
||||
&String::new()
|
||||
};
|
||||
egui::ComboBox::from_id_source("device_select")
|
||||
.selected_text(selected_text)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, device) in self.video_devices.iter().enumerate() {
|
||||
ui.selectable_value(&mut self.selected_device_idx, i, device);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(33));
|
||||
ui.add_space(6.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("推流信息")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("推流地址:");
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"rtsp://{}:8554/{}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
))
|
||||
.color(Color32::from_rgb(100, 149, 237)),
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("观看地址:");
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"http://{}:8889/webrtc.html?src={}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
))
|
||||
.color(Color32::from_rgb(100, 149, 237)),
|
||||
);
|
||||
});
|
||||
|
||||
if !self.video_devices.is_empty() && self.selected_device_idx < self.video_devices.len() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("当前设备:");
|
||||
ui.label(
|
||||
RichText::new(&self.video_devices[self.selected_device_idx])
|
||||
.color(Color32::from_rgb(144, 238, 144)),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
let (btn_text, btn_color) = if self.is_streaming {
|
||||
("停止推流", Color32::from_rgb(183, 28, 28))
|
||||
} else {
|
||||
("开始推流", Color32::from_rgb(46, 125, 50))
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_sized(
|
||||
[ui.available_width(), 44.0],
|
||||
egui::Button::new(
|
||||
RichText::new(btn_text)
|
||||
.font(FontId::new(16.0, FontFamily::Proportional))
|
||||
.color(Color32::WHITE),
|
||||
)
|
||||
.fill(btn_color)
|
||||
.rounding(8.0),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if self.is_streaming {
|
||||
self.stop_stream();
|
||||
} else {
|
||||
self.start_stream();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("日志")
|
||||
.font(FontId::new(12.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(
|
||||
RichText::new(&self.log_message)
|
||||
.font(FontId::new(11.0, FontFamily::Proportional))
|
||||
.color(Color32::from_rgb(200, 200, 200)),
|
||||
);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("⚙ 设置").clicked() {
|
||||
self.temp_config = self.config.clone();
|
||||
self.show_settings = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if self.show_settings {
|
||||
egui::Window::new("设置")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.fixed_size([480.0, 220.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("FFmpeg 路径:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.ffmpeg_path);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("服务器 IP:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.server_ip);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("推流路径:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.stream_path);
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("确定").clicked() {
|
||||
self.config = self.temp_config.clone();
|
||||
self.config.save();
|
||||
self.show_settings = false;
|
||||
self.start_status_thread();
|
||||
self.log_message = "配置已更新".to_string();
|
||||
}
|
||||
if ui.button("取消").clicked() {
|
||||
self.show_settings = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
let options = NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([960.0, 600.0])
|
||||
.with_inner_size([560.0, 480.0])
|
||||
.with_resizable(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"视频采集卡查看器",
|
||||
"视频采集卡推流",
|
||||
options,
|
||||
Box::new(|_cc| Box::new(CaptureCardViewer::new())),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user