refactor: 改用FFmpeg枚举DirectShow设备并推流到MediaMTX,移除Nokhwa依赖

This commit is contained in:
xiaji
2026-04-14 11:00:08 +08:00
parent d5e0791f3c
commit 78b50e8ecf
3 changed files with 543 additions and 471 deletions

View File

@@ -77,12 +77,6 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -158,12 +152,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@@ -180,7 +168,7 @@ dependencies = [
"log", "log",
"nix 0.25.1", "nix 0.25.1",
"slotmap", "slotmap",
"thiserror 1.0.69", "thiserror",
"vec_map", "vec_map",
] ]
@@ -190,7 +178,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"eframe", "eframe",
"egui", "egui",
"nokhwa", "serde",
"serde_json",
"winapi", "winapi",
] ]
@@ -379,12 +368,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "ecolor" name = "ecolor"
version = "0.24.1" version = "0.24.1"
@@ -408,7 +391,7 @@ dependencies = [
"glow", "glow",
"glutin", "glutin",
"glutin-winit", "glutin-winit",
"image 0.24.9", "image",
"js-sys", "js-sys",
"log", "log",
"objc", "objc",
@@ -416,7 +399,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"static_assertions", "static_assertions",
"thiserror 1.0.69", "thiserror",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
@@ -539,18 +522,6 @@ dependencies = [
"miniz_oxide", "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]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -581,12 +552,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.32" version = "0.3.32"
@@ -615,19 +580,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@@ -858,18 +810,6 @@ dependencies = [
"png", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@@ -892,6 +832,12 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.21.1" version = "0.21.1"
@@ -903,7 +849,7 @@ dependencies = [
"combine", "combine",
"jni-sys 0.3.1", "jni-sys 0.3.1",
"log", "log",
"thiserror 1.0.69", "thiserror",
"walkdir", "walkdir",
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
@@ -942,7 +888,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom",
"libc", "libc",
] ]
@@ -1099,60 +1045,6 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "ndk" name = "ndk"
version = "0.7.0" version = "0.7.0"
@@ -1164,7 +1056,7 @@ dependencies = [
"ndk-sys", "ndk-sys",
"num_enum 0.5.11", "num_enum 0.5.11",
"raw-window-handle", "raw-window-handle",
"thiserror 1.0.69", "thiserror",
] ]
[[package]] [[package]]
@@ -1213,43 +1105,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -1457,12 +1312,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -1528,12 +1377,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -1582,15 +1425,6 @@ dependencies = [
"bitflags 2.11.0", "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]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@@ -1638,6 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@@ -1660,6 +1495,19 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -1722,15 +1570,6 @@ dependencies = [
"wayland-client", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@@ -1782,16 +1621,7 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl 1.0.69", "thiserror-impl",
]
[[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",
] ]
[[package]] [[package]]
@@ -1805,17 +1635,6 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@@ -2118,107 +1937,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"
@@ -2302,15 +2026,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1", "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]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.42.2" version = "0.42.2"
@@ -2634,3 +2349,9 @@ dependencies = [
"quote", "quote",
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
nokhwa = { version = "0.10", features = ["input-msmf"] }
eframe = { version = "0.24", default-features = false, features = ["default_fonts", "glow"] } eframe = { version = "0.24", default-features = false, features = ["default_fonts", "glow"] }
egui = "0.24" egui = "0.24"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
winapi = { version = "0.3", features = ["winuser", "windef", "processthreadsapi", "handleapi", "winbase"] } winapi = { version = "0.3", features = ["winuser", "windef", "processthreadsapi", "handleapi", "winbase"] }
[profile.release] [profile.release]

View File

@@ -1,232 +1,582 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use eframe::{egui, App, Frame, NativeOptions}; use eframe::{egui, App, Frame, NativeOptions};
use egui::{Color32, FontFamily, FontId, RichText, TextureHandle, TextureOptions}; use egui::{Color32, FontFamily, FontId, RichText};
use nokhwa::utils::{ApiBackend, RequestedFormat, RequestedFormatType}; use serde::{Deserialize, Serialize};
use nokhwa::Camera; use std::fs;
use std::sync::{Arc, Mutex}; 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::thread;
use std::time::Duration; use std::time::Duration;
struct CaptureState { #[derive(Debug, Clone, Serialize, Deserialize)]
device_name: Arc<Mutex<String>>, struct Config {
frame_data: Arc<Mutex<Option<(usize, usize, Vec<u8>)>>>, ffmpeg_path: String,
running: Arc<Mutex<bool>>, server_ip: String,
error: Arc<Mutex<Option<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 { struct CaptureCardViewer {
state: CaptureState, config: Config,
texture: Option<TextureHandle>, 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 { impl CaptureCardViewer {
fn new() -> Self { fn new() -> Self {
let state = CaptureState { let config = Config::load();
device_name: Arc::new(Mutex::new(String::new())), let (tx, rx) = channel();
frame_data: Arc::new(Mutex::new(None)),
running: Arc::new(Mutex::new(true)), let ffmpeg_ok = Path::new(&config.ffmpeg_path).exists();
error: Arc::new(Mutex::new(None)), 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 { app.scan_devices();
device_name: state.device_name.clone(), app.start_status_thread();
frame_data: state.frame_data.clone(),
running: state.running.clone(), app
error: state.error.clone(), }
};
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 || { thread::spawn(move || {
capture_thread(capture_state); let result = enumerate_dshow_devices(&ffmpeg_path);
match result {
Ok(devices) => {
if devices.is_empty() {
let _ = tx.send(Message::ScanFailed(
"未找到任何视频采集设备,请确认设备已连接".to_string(),
));
} else {
let _ = tx.send(Message::DevicesFound(devices));
}
}
Err(e) => {
let _ = tx.send(Message::ScanFailed(format!("扫描失败: {}", e)));
}
}
}); });
}
CaptureCardViewer { fn start_status_thread(&self) {
state, let tx = self.tx.clone();
texture: None, 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_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 args = vec![
"-f",
"dshow",
"-i",
&video_input,
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&output_url,
];
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);
}
}
}
fn stop_stream(&mut self) {
let _ = Command::new("taskkill")
.args(&["/F", "/IM", "ffmpeg.exe"])
.output();
self.is_streaming = false;
self.log_message = "推流已停止".to_string();
} }
} }
fn capture_thread(state: CaptureState) { fn enumerate_dshow_devices(ffmpeg_path: &str) -> Result<Vec<String>, String> {
let devices = match nokhwa::query(ApiBackend::MediaFoundation) { if !Path::new(ffmpeg_path).exists() {
Ok(d) => d, return Err(format!("FFmpeg 不存在: {}", ffmpeg_path));
Err(e) => {
*state.error.lock().unwrap() = Some(format!("查询设备失败: {}", e));
return;
}
};
if devices.is_empty() {
*state.error.lock().unwrap() = Some("未找到视频采集设备".to_string());
return;
} }
let device_info = &devices[0]; let output = Command::new(ffmpeg_path)
*state.device_name.lock().unwrap() = device_info.human_name().to_string(); .args(&["-list_devices", "true", "-f", "dshow", "-i", "dummy"])
.stderr(Stdio::piped())
.stdout(Stdio::null())
.output()
.map_err(|e| format!("执行 FFmpeg 失败: {}", e))?;
let index = device_info.index().clone(); let stderr = String::from_utf8_lossy(&output.stderr);
let requested = RequestedFormat::new::<nokhwa::pixel_format::RgbFormat>( let mut devices = Vec::new();
RequestedFormatType::AbsoluteHighestFrameRate, let mut is_video = false;
);
let mut camera = match Camera::new(index, requested) { for line in stderr.lines() {
Ok(c) => c, let trimmed = line.trim();
Err(e) => {
*state.error.lock().unwrap() = Some(format!("打开摄像头失败: {}", e)); if trimmed.contains("(video)") {
return; is_video = true;
} else if trimmed.contains("(audio)") {
is_video = false;
} }
};
if let Err(e) = camera.open_stream() { if is_video {
*state.error.lock().unwrap() = Some(format!("启动视频流失败: {}", e)); if let Some(name) = extract_device_name(trimmed) {
return; if !devices.contains(&name) {
} devices.push(name);
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);
}
*state.frame_data.lock().unwrap() = Some((w, h, rgba));
*state.error.lock().unwrap() = None;
}
Err(e) => {
*state.error.lock().unwrap() = Some(format!("解码帧失败: {}", e));
}
} }
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;
}
}
} }
impl App for CaptureCardViewer { impl App for CaptureCardViewer {
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
static FONT_LOADED: std::sync::Once = std::sync::Once::new(); static FONT_LOADED: std::sync::Once = std::sync::Once::new();
FONT_LOADED.call_once(|| { FONT_LOADED.call_once(|| {
let mut fonts = egui::FontDefinitions::default(); configure_fonts(ctx);
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;
}
}
}); });
let frame_update = { while let Ok(msg) = self.rx.try_recv() {
let frame_guard = self.state.frame_data.lock().unwrap(); match msg {
frame_guard.as_ref().map(|(w, h, rgba)| { Message::DevicesFound(devices) => {
if *w > 0 && *h > 0 { self.video_devices = devices;
let color_image = egui::ColorImage::from_rgba_unmultiplied([*w, *h], rgba); self.selected_device_idx = 0;
Some(color_image) self.is_scanning = false;
} else { if self.video_devices.is_empty() {
None self.log_message = "未找到视频采集设备".to_string();
} else {
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;
} }
})
};
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),
);
} }
} }
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(10.0); ui.add_space(8.0);
let device_name = self.state.device_name.lock().unwrap().clone();
ui.label( ui.label(
RichText::new(if device_name.is_empty() { RichText::new("视频采集卡推流工具")
"正在查找设备..." .font(FontId::new(20.0, FontFamily::Proportional))
} else { .strong(),
&device_name
})
.font(FontId::new(18.0, FontFamily::Proportional))
.strong(),
); );
});
ui.add_space(8.0);
ui.add_space(5.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);
let error = self.state.error.lock().unwrap(); ui.horizontal(|ui| {
if let Some(e) = error.as_ref() { 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);
}
});
}
});
});
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( ui.label(
RichText::new(e) RichText::new(format!(
.font(FontId::new(12.0, FontFamily::Proportional)) "rtsp://{}:8554/{}",
.color(Color32::RED), 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)),
);
});
} }
drop(error); });
ui.add_space(5.0); ui.add_space(10.0);
if let Some(tex) = &self.texture { ui.vertical_centered(|ui| {
let avail_width = ui.available_width(); let (btn_text, btn_color) = if self.is_streaming {
let size = tex.size_vec2(); ("停止推流", Color32::from_rgb(183, 28, 28))
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 { } else {
ui.add_space(100.0); ("开始推流", Color32::from_rgb(46, 125, 50))
ui.label( };
RichText::new("等待视频画面...")
.font(FontId::new(14.0, FontFamily::Proportional)) if ui
.color(Color32::from_rgb(128, 128, 128)), .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;
} }
}); });
}); });
ctx.request_repaint_after(Duration::from_millis(33)); 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> { fn main() -> Result<(), eframe::Error> {
let options = NativeOptions { let options = NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([960.0, 600.0]) .with_inner_size([560.0, 480.0])
.with_resizable(true), .with_resizable(true),
..Default::default() ..Default::default()
}; };
eframe::run_native( eframe::run_native(
"视频采集卡查看器", "视频采集卡推流",
options, options,
Box::new(|_cc| Box::new(CaptureCardViewer::new())), Box::new(|_cc| Box::new(CaptureCardViewer::new())),
) )