Added check on first frame feed

This commit is contained in:
2026-06-29 21:20:54 -03:00
parent d83acce887
commit 5ed9cfbc2a
5 changed files with 1326 additions and 2 deletions
+260 -1
View File
@@ -10,12 +10,24 @@ pub enum Mode {
Local,
Coordinator,
Worker,
Rtsp,
RedisCoordinator,
RedisWorker,
}
#[derive(Debug, Clone)]
pub struct Config {
pub mode: Mode,
pub cidrs: Vec<String>,
pub rtsp_targets: Vec<String>,
pub rtsp_paths: Vec<String>,
pub redis_url: String,
pub redis_queue_name: String,
pub redis_input_csv: String,
pub redis_worker_count: usize,
pub snapshot_dir: PathBuf,
pub sqlite_path: PathBuf,
pub capture_timeout_ms: u64,
pub port: u16,
pub timeout_ms: u64,
pub concurrency: usize,
@@ -33,9 +45,36 @@ impl Config {
let cli = CliArgs::from_args(&args)?;
let cidrs = load_cidrs(cli.cidr_file.as_deref(), cli.cidrs.as_deref())?;
let rtsp_targets = load_csv_targets(cli.targets_file.as_deref(), cli.targets.as_deref())?;
let rtsp_paths = load_rtsp_paths(cli.paths_file.as_deref(), cli.paths.as_deref())?;
Ok(Self {
mode,
cidrs,
rtsp_targets,
rtsp_paths,
redis_url: cli
.redis_url
.unwrap_or_else(|| read_env("REDIS_URL", "redis://127.0.0.1:6379/0")),
redis_queue_name: cli
.redis_queue_name
.unwrap_or_else(|| read_env("RTSP_QUEUE_NAME", "camfinder:rtsp:queue")),
redis_input_csv: cli
.redis_input_csv
.unwrap_or_else(|| read_env("RTSP_INPUT_CSV", "camfinder-open-rtsp.csv")),
redis_worker_count: cli
.redis_worker_count
.or_else(|| parse_usize("RTSP_WORKER_COUNT", 2).ok())
.unwrap_or(2),
snapshot_dir: cli
.snapshot_dir
.unwrap_or_else(|| PathBuf::from(read_env("RTSP_SNAPSHOT_DIR", "dump/feeds"))),
sqlite_path: cli
.sqlite_path
.unwrap_or_else(|| PathBuf::from(read_env("RTSP_SQLITE_PATH", "dump/rtsp-results.sqlite"))),
capture_timeout_ms: cli
.capture_timeout_ms
.or_else(|| parse_u64("RTSP_CAPTURE_TIMEOUT_MS", 15000).ok())
.unwrap_or(15000),
port: cli
.port
.or_else(|| parse_u16("SCAN_PORT", 554).ok())
@@ -69,6 +108,17 @@ impl Config {
struct CliArgs {
cidrs: Option<String>,
cidr_file: Option<PathBuf>,
targets: Option<String>,
targets_file: Option<PathBuf>,
paths: Option<String>,
paths_file: Option<PathBuf>,
redis_url: Option<String>,
redis_queue_name: Option<String>,
redis_input_csv: Option<String>,
redis_worker_count: Option<usize>,
snapshot_dir: Option<PathBuf>,
sqlite_path: Option<PathBuf>,
capture_timeout_ms: Option<u64>,
database_url: Option<String>,
coordinator_bind: Option<SocketAddr>,
coordinator_addr: Option<String>,
@@ -94,6 +144,54 @@ impl CliArgs {
cli.cidr_file =
Some(PathBuf::from(next_value(args, &mut index, "--cidr-file")?));
}
"--targets" => {
cli.targets = Some(next_value(args, &mut index, "--targets")?);
}
"--targets-file" => {
cli.targets_file = Some(PathBuf::from(next_value(
args,
&mut index,
"--targets-file",
)?));
}
"--paths" => {
cli.paths = Some(next_value(args, &mut index, "--paths")?);
}
"--paths-file" => {
cli.paths_file =
Some(PathBuf::from(next_value(args, &mut index, "--paths-file")?));
}
"--redis-url" => {
cli.redis_url = Some(next_value(args, &mut index, "--redis-url")?);
}
"--queue" | "--queue-name" => {
cli.redis_queue_name = Some(next_value(args, &mut index, arg)?);
}
"--input-csv" | "--csv-file" => {
cli.redis_input_csv = Some(next_value(args, &mut index, arg)?);
}
"--worker-count" => {
cli.redis_worker_count = Some(parse_usize_literal(&next_value(
args,
&mut index,
"--worker-count",
)?)?);
}
"--snapshot-dir" => {
cli.snapshot_dir =
Some(PathBuf::from(next_value(args, &mut index, "--snapshot-dir")?));
}
"--sqlite-path" => {
cli.sqlite_path =
Some(PathBuf::from(next_value(args, &mut index, "--sqlite-path")?));
}
"--capture-timeout-ms" => {
cli.capture_timeout_ms = Some(parse_u64_literal(&next_value(
args,
&mut index,
"--capture-timeout-ms",
)?)?);
}
"--database" | "--database-url" => {
cli.database_url = Some(next_value(args, &mut index, arg)?);
}
@@ -167,8 +265,11 @@ fn parse_mode_literal(value: &str) -> Result<Mode> {
"local" | "scan" => Ok(Mode::Local),
"coordinator" | "coord" | "server" => Ok(Mode::Coordinator),
"worker" => Ok(Mode::Worker),
"rtsp" => Ok(Mode::Rtsp),
"redis-coordinator" | "redis-coord" | "redis-server" => Ok(Mode::RedisCoordinator),
"redis-worker" | "redis-client" => Ok(Mode::RedisWorker),
other => Err(anyhow!(
"modo inválido: {other}. Use local, coordinator ou worker"
"modo inválido: {other}. Use local, coordinator, worker, rtsp, redis-coordinator ou redis-worker"
)),
}
}
@@ -238,6 +339,164 @@ fn parse_cidr_list(input: &str) -> Vec<String> {
.collect()
}
fn load_csv_targets(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<String>> {
if let Some(inline) = inline {
let targets = parse_target_list(inline);
if !targets.is_empty() {
return Ok(targets);
}
}
if let Some(file) = file {
let content = fs::read_to_string(file)
.with_context(|| format!("failed to read CSV file {}", file.display()))?;
let targets = parse_csv_targets(&content);
if !targets.is_empty() {
return Ok(targets);
}
return Err(anyhow!("arquivo CSV vazio: {}", file.display()));
}
if let Ok(path) = env::var("RTSP_TARGETS_FILE") {
let path = path.trim();
if !path.is_empty() {
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read CSV file {path}"))?;
let targets = parse_csv_targets(&content);
if !targets.is_empty() {
return Ok(targets);
}
return Err(anyhow!("arquivo CSV vazio: {path}"));
}
}
if let Ok(value) = env::var("RTSP_TARGETS") {
let value = value.trim();
if !value.is_empty() {
let targets = parse_target_list(value);
if !targets.is_empty() {
return Ok(targets);
}
}
}
Ok(Vec::new())
}
fn parse_csv_targets(input: &str) -> Vec<String> {
input
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split(',').next())
.map(str::trim)
.map(strip_quotes)
.filter(|entry| !entry.is_empty())
.filter(|entry| !is_csv_header(entry))
.map(ToString::to_string)
.collect()
}
fn parse_target_list(input: &str) -> Vec<String> {
input
.lines()
.flat_map(|line| line.split(|c: char| c == ',' || c.is_whitespace()))
.map(str::trim)
.map(strip_quotes)
.filter(|entry| !entry.is_empty())
.filter(|entry| !is_csv_header(entry))
.map(ToString::to_string)
.collect()
}
fn load_rtsp_paths(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<String>> {
if let Some(inline) = inline {
let paths = parse_rtsp_paths(inline);
if !paths.is_empty() {
return Ok(paths);
}
}
if let Some(file) = file {
let content = fs::read_to_string(file)
.with_context(|| format!("failed to read RTSP path file {}", file.display()))?;
let paths = parse_rtsp_paths(&content);
if !paths.is_empty() {
return Ok(paths);
}
return Err(anyhow!("arquivo de paths RTSP vazio: {}", file.display()));
}
if let Ok(path) = env::var("RTSP_PATHS_FILE") {
let path = path.trim();
if !path.is_empty() {
return load_rtsp_paths_from_file(Path::new(path));
}
}
if let Ok(value) = env::var("RTSP_PATHS") {
let value = value.trim();
if !value.is_empty() {
let paths = parse_rtsp_paths(value);
if !paths.is_empty() {
return Ok(paths);
}
}
}
load_rtsp_paths_from_file(Path::new("rtsp_paths.txt"))
}
fn load_rtsp_paths_from_file(file: &Path) -> Result<Vec<String>> {
let content = fs::read_to_string(file)
.with_context(|| format!("failed to read RTSP path file {}", file.display()))?;
let paths = parse_rtsp_paths(&content);
if !paths.is_empty() {
return Ok(paths);
}
Err(anyhow!("arquivo de paths RTSP vazio: {}", file.display()))
}
fn parse_rtsp_paths(input: &str) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
input
.lines()
.flat_map(|line| line.split('#').next())
.flat_map(|line| line.split(|c: char| c == ',' || c.is_whitespace()))
.map(str::trim)
.map(strip_quotes)
.filter(|entry| !entry.is_empty())
.filter(|entry| entry.starts_with('/'))
.filter_map(|entry| {
let entry = entry.to_string();
if seen.insert(entry.clone()) {
Some(entry)
} else {
None
}
})
.collect()
}
fn strip_quotes(value: &str) -> &str {
value
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| {
value
.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
})
.unwrap_or(value)
}
fn is_csv_header(value: &str) -> bool {
matches!(
value.to_ascii_lowercase().as_str(),
"ip" | "host" | "hostname" | "address" | "target" | "camera"
)
}
fn database_url_from_env() -> String {
let value = read_env("DATABASE_URL", "");
if value.is_empty() {