feat: trace-level matching, health watcher/worker status, timezone config
This commit is contained in:
@@ -14,8 +14,16 @@ use crate::core::db::{schema, PostgresDb};
|
||||
|
||||
/// Shared video query params: mode=normal|debug, audio=on|off
|
||||
fn parse_video_params(params: &std::collections::HashMap<String, String>) -> (String, String) {
|
||||
let mode = params.get("mode").map(|s| s.as_str()).unwrap_or("normal").to_string();
|
||||
let audio = params.get("audio").map(|s| s.as_str()).unwrap_or("on").to_string();
|
||||
let mode = params
|
||||
.get("mode")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("normal")
|
||||
.to_string();
|
||||
let audio = params
|
||||
.get("audio")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("on")
|
||||
.to_string();
|
||||
(mode, audio)
|
||||
}
|
||||
|
||||
@@ -142,9 +150,12 @@ struct BboxParams {
|
||||
/// Priority: start_frame/end_frame > start/end > start_time/end_time.
|
||||
/// If only time is given, convert via fps.
|
||||
fn resolve_frame_range(
|
||||
start_frame: Option<i32>, end_frame: Option<i32>,
|
||||
start: Option<i32>, end: Option<i32>,
|
||||
start_time: Option<f64>, end_time: Option<f64>,
|
||||
start_frame: Option<i32>,
|
||||
end_frame: Option<i32>,
|
||||
start: Option<i32>,
|
||||
end: Option<i32>,
|
||||
start_time: Option<f64>,
|
||||
end_time: Option<f64>,
|
||||
fps: f64,
|
||||
) -> (i32, i32) {
|
||||
if let (Some(sf), Some(ef)) = (start_frame.or(start), end_frame.or(end)) {
|
||||
@@ -186,7 +197,15 @@ async fn bbox_overlay_video(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.unwrap_or(24.0);
|
||||
|
||||
let (start_f, end_f) = resolve_frame_range(p.start_frame, p.end_frame, p.start, p.end, p.start_time, p.end_time, fps);
|
||||
let (start_f, end_f) = resolve_frame_range(
|
||||
p.start_frame,
|
||||
p.end_frame,
|
||||
p.start,
|
||||
p.end,
|
||||
p.start_time,
|
||||
p.end_time,
|
||||
fps,
|
||||
);
|
||||
|
||||
let start_sec = start_f as f64 / fps;
|
||||
|
||||
@@ -228,13 +247,26 @@ async fn bbox_overlay_video(
|
||||
let dur = duration.to_string();
|
||||
let mut bbox_args = vec!["-ss", &ss, "-i", &video_path, "-t", &dur];
|
||||
if vf != "null" {
|
||||
bbox_args.extend_from_slice(&["-vf", &vf, "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"]);
|
||||
bbox_args.extend_from_slice(&[
|
||||
"-vf",
|
||||
&vf,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-crf",
|
||||
"28",
|
||||
]);
|
||||
} else {
|
||||
bbox_args.extend_from_slice(&["-c", "copy"]);
|
||||
}
|
||||
if bbox_audio == "off" { bbox_args.push("-an"); }
|
||||
if bbox_audio == "off" {
|
||||
bbox_args.push("-an");
|
||||
}
|
||||
bbox_args.extend_from_slice(&["-movflags", "+faststart", "-y", &tmp_str]);
|
||||
let status = ffmpeg_cmd().args(&bbox_args).status()
|
||||
let status = ffmpeg_cmd()
|
||||
.args(&bbox_args)
|
||||
.status()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !status.success() {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
@@ -315,14 +347,20 @@ async fn trace_video(
|
||||
let sk = seek.to_string();
|
||||
let du = duration.to_string();
|
||||
let mut cmd_args = vec!["-ss", &sk, "-i", &video_path, "-t", &du, "-c", "copy"];
|
||||
if audio == "off" { cmd_args.push("-an"); }
|
||||
if audio == "off" {
|
||||
cmd_args.push("-an");
|
||||
}
|
||||
cmd_args.extend_from_slice(&["-y", &tmp_str]);
|
||||
let result = ffmpeg_cmd().args(&cmd_args).output()
|
||||
let result = ffmpeg_cmd()
|
||||
.args(&cmd_args)
|
||||
.output()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !result.status.success() {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let data = tokio::fs::read(&tmp)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Ok(Response::builder()
|
||||
.header(header::CONTENT_TYPE, "video/mp4")
|
||||
@@ -345,8 +383,11 @@ async fn trace_video(
|
||||
ORDER BY fd.trace_id, fd.frame_number",
|
||||
face_table, identities_table
|
||||
))
|
||||
.bind(&file_uuid).bind(start_fn).bind(end_fn)
|
||||
.fetch_all(state.db.pool()).await
|
||||
.bind(&file_uuid)
|
||||
.bind(start_fn)
|
||||
.bind(end_fn)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// Group frames by trace_id, compute start_frame per trace; collect bbox per frame
|
||||
@@ -359,7 +400,9 @@ async fn trace_video(
|
||||
if let Some(name) = name_opt {
|
||||
trace_identity.entry(*tid).or_insert_with(|| name.clone());
|
||||
} else {
|
||||
trace_identity.entry(*tid).or_insert_with(|| format!("Stranger_{:03}", tid));
|
||||
trace_identity
|
||||
.entry(*tid)
|
||||
.or_insert_with(|| format!("Stranger_{:03}", tid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +417,8 @@ async fn trace_video(
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
|
||||
// Sort traces for consistent ordering
|
||||
let mut sorted_traces: Vec<(i32, &Vec<i32>)> = trace_frames.iter().map(|(k, v)| (*k, v)).collect();
|
||||
let mut sorted_traces: Vec<(i32, &Vec<i32>)> =
|
||||
trace_frames.iter().map(|(k, v)| (*k, v)).collect();
|
||||
sorted_traces.sort_by_key(|(tid, _)| *tid);
|
||||
|
||||
let frame_offset = first_frame as i64 - (padding * fps) as i64;
|
||||
@@ -389,10 +433,12 @@ async fn trace_video(
|
||||
"drawtext=text='Frame %{{n}} %{{pts}}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=12"
|
||||
));
|
||||
parts.push(format!(
|
||||
"drawtext=text='Cut\\: {}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=56", cut_label
|
||||
"drawtext=text='Cut\\: {}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=56",
|
||||
cut_label
|
||||
));
|
||||
parts.push(format!(
|
||||
"drawtext=text='{}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=100", file_uuid
|
||||
"drawtext=text='{}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=100",
|
||||
file_uuid
|
||||
));
|
||||
|
||||
// Per-trace entries: show trace_id, start_frame, identity name
|
||||
@@ -400,11 +446,18 @@ async fn trace_video(
|
||||
let mut y_pos = 144;
|
||||
for (tid, frames) in &sorted_traces {
|
||||
let start = frames.iter().min().unwrap_or(&first_frame);
|
||||
let identity = trace_identity.get(tid).map(|s| s.as_str()).unwrap_or("unknown");
|
||||
let identity = trace_identity
|
||||
.get(tid)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let label = format!("Trace {}\\: start={} {}", tid, start, identity);
|
||||
|
||||
// Continuous range (interpolated): visible from first to last frame
|
||||
let enable = format!("between(n,{},{})", frames[0] as i64 - frame_offset, frames[frames.len() - 1] as i64 - frame_offset);
|
||||
let enable = format!(
|
||||
"between(n,{},{})",
|
||||
frames[0] as i64 - frame_offset,
|
||||
frames[frames.len() - 1] as i64 - frame_offset
|
||||
);
|
||||
|
||||
parts.push(format!(
|
||||
"drawtext=text='{}':fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y={}:enable='{}'",
|
||||
@@ -415,7 +468,11 @@ async fn trace_video(
|
||||
|
||||
// Bounding boxes: interpolated (thickness=1) + actual (thickness=4) with trace_id label
|
||||
for (tid, frames) in &sorted_traces {
|
||||
let range_enable = format!("between(n,{},{})", frames[0] as i64 - frame_offset, frames[frames.len() - 1] as i64 - frame_offset);
|
||||
let range_enable = format!(
|
||||
"between(n,{},{})",
|
||||
frames[0] as i64 - frame_offset,
|
||||
frames[frames.len() - 1] as i64 - frame_offset
|
||||
);
|
||||
// Interpolated bbox at first known position across the whole trace range
|
||||
if let Some((x, y, w, h)) = bbox_per_frame.get(&(*tid, frames[0])) {
|
||||
parts.push(format!(
|
||||
@@ -448,23 +505,45 @@ async fn trace_video(
|
||||
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
||||
let sk = seek.to_string();
|
||||
let du = duration.to_string();
|
||||
let mut debug_args = vec!["-ss", &sk, "-i", &video_path, "-t", &du,
|
||||
"-/filter_complex", &filter_path,
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"];
|
||||
if audio == "on" { debug_args.extend_from_slice(&["-c:a", "aac"]); }
|
||||
let mut debug_args = vec![
|
||||
"-ss",
|
||||
&sk,
|
||||
"-i",
|
||||
&video_path,
|
||||
"-t",
|
||||
&du,
|
||||
"-/filter_complex",
|
||||
&filter_path,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-crf",
|
||||
"28",
|
||||
];
|
||||
if audio == "on" {
|
||||
debug_args.extend_from_slice(&["-c:a", "aac"]);
|
||||
}
|
||||
debug_args.extend_from_slice(&["-movflags", "+faststart", "-y", &tmp_str]);
|
||||
let result = ffmpeg_cmd().args(&debug_args).output()
|
||||
let result = ffmpeg_cmd()
|
||||
.args(&debug_args)
|
||||
.output()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !result.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
let _ = std::fs::write("/tmp/ffmpeg_last_error.txt", stderr.as_bytes());
|
||||
tracing::error!("ffmpeg failed ({} bytes), see /tmp/ffmpeg_last_error.txt", stderr.len());
|
||||
tracing::error!(
|
||||
"ffmpeg failed ({} bytes), see /tmp/ffmpeg_last_error.txt",
|
||||
stderr.len()
|
||||
);
|
||||
let _ = std::fs::remove_file(&filter_file);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let data = tokio::fs::read(&tmp)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let _ = std::fs::remove_file(&filter_file);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
Ok(Response::builder()
|
||||
@@ -503,19 +582,27 @@ async fn stream_video(
|
||||
// Chunk extraction with dual time/frame params
|
||||
let start_time_param = params.get("start_time").and_then(|v| v.parse::<f64>().ok());
|
||||
let end_time_param = params.get("end_time").and_then(|v| v.parse::<f64>().ok());
|
||||
let start_frame_param = params.get("start_frame").and_then(|v| v.parse::<f64>().ok());
|
||||
let start_frame_param = params
|
||||
.get("start_frame")
|
||||
.and_then(|v| v.parse::<f64>().ok());
|
||||
let end_frame_param = params.get("end_frame").and_then(|v| v.parse::<f64>().ok());
|
||||
let start_legacy = params.get("start").and_then(|v| v.parse::<f64>().ok());
|
||||
let end_legacy = params.get("end").and_then(|v| v.parse::<f64>().ok());
|
||||
|
||||
let has_range = start_frame_param.is_some() || start_time_param.is_some() || start_legacy.is_some();
|
||||
let has_range =
|
||||
start_frame_param.is_some() || start_time_param.is_some() || start_legacy.is_some();
|
||||
|
||||
if has_range {
|
||||
let (start_sec, dur) = if let (Some(sf), Some(ef)) = (start_frame_param, end_frame_param) {
|
||||
let _fps: f64 = sqlx::query_scalar(&format!(
|
||||
"SELECT COALESCE(fps, 24.0) FROM {} WHERE file_uuid = $1", videos_table
|
||||
)).bind(&file_uuid).fetch_optional(state.db.pool()).await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?.unwrap_or(24.0);
|
||||
"SELECT COALESCE(fps, 24.0) FROM {} WHERE file_uuid = $1",
|
||||
videos_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.unwrap_or(24.0);
|
||||
(sf / _fps, (ef - sf) / _fps)
|
||||
} else if let (Some(st), Some(et)) = (start_time_param, end_time_param) {
|
||||
(st, et - st)
|
||||
@@ -533,15 +620,21 @@ async fn stream_video(
|
||||
let ss = start_sec.to_string();
|
||||
let d = dur.to_string();
|
||||
let mut chunk_args = vec!["-ss", &ss, "-i", &file_path, "-t", &d, "-c", "copy"];
|
||||
if audio == "off" { chunk_args.push("-an"); }
|
||||
if audio == "off" {
|
||||
chunk_args.push("-an");
|
||||
}
|
||||
chunk_args.extend_from_slice(&["-movflags", "+faststart", "-y", &tmp_str]);
|
||||
let status = ffmpeg_cmd().args(&chunk_args).status()
|
||||
let status = ffmpeg_cmd()
|
||||
.args(&chunk_args)
|
||||
.status()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !status.success() {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let data = tokio::fs::read(&tmp)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Ok(Response::builder()
|
||||
.header(header::CONTENT_TYPE, "video/mp4")
|
||||
@@ -704,7 +797,7 @@ async fn video_clip(
|
||||
let frame_count = ((e - s) * fps) as i64;
|
||||
cmd.args(["-vframes", &frame_count.to_string()]);
|
||||
} else {
|
||||
cmd.args(["-to", &e.to_string()]);
|
||||
cmd.args(["-t", &(e - s).to_string()]);
|
||||
}
|
||||
if mode == "debug" {
|
||||
let debug_text = if let (Some(sf), Some(ef)) = (q.start_frame, q.end_frame) {
|
||||
@@ -717,8 +810,20 @@ async fn video_clip(
|
||||
if audio == "off" {
|
||||
cmd.args(["-an"]);
|
||||
}
|
||||
cmd.args(["-c:v", "libx264", "-c:a", "aac", "-f", "mpegts", "-"]);
|
||||
let output = cmd.output().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
cmd.args([
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-movflags",
|
||||
"frag_keyframe+empty_moov",
|
||||
"-f",
|
||||
"mp4",
|
||||
"-",
|
||||
]);
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
if !output.status.success() {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user