Fix trace/3128: drawtext + filter_complex_script
- Replaced bitmap font (~195K drawbox commands) with drawtext (~2.2K) - Write filter to temp file, use -/filter_complex to bypass ARG_MAX - Added ffmpeg stderr logging for debugging
This commit is contained in:
+23
-40
@@ -297,64 +297,46 @@ async fn trace_video(
|
|||||||
let duration = (last_frame - first_frame) as f64 / fps + padding * 2.0;
|
let duration = (last_frame - first_frame) as f64 / fps + padding * 2.0;
|
||||||
let seek = (start_sec - padding).max(0.0);
|
let seek = (start_sec - padding).max(0.0);
|
||||||
|
|
||||||
// Build filters: bbox+text holding at last detection until next one
|
// Build filters: bbox+drawtext (1 filter + 1 drawtext per detection)
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
for (i, (frame, x, y, w, h)) in rows.iter().enumerate() {
|
for (i, (frame, x, y, w, h)) in rows.iter().enumerate() {
|
||||||
// Hold this detection until the next one (or end)
|
let next_frame = if i + 1 < rows.len() { rows[i + 1].0 } else { last_frame + (padding * fps) as i32 };
|
||||||
let next_frame = if i + 1 < rows.len() {
|
|
||||||
rows[i + 1].0
|
|
||||||
} else {
|
|
||||||
// For last detection, extend to duration end
|
|
||||||
last_frame + (padding * fps) as i32
|
|
||||||
};
|
|
||||||
let start_offset = frame - first_frame + (padding * fps) as i32;
|
let start_offset = frame - first_frame + (padding * fps) as i32;
|
||||||
let end_offset = next_frame - first_frame + (padding * fps) as i32;
|
let end_offset = next_frame - first_frame + (padding * fps) as i32;
|
||||||
|
// Bbox
|
||||||
// Bbox: visible from this frame until next detection
|
|
||||||
parts.push(format!(
|
parts.push(format!(
|
||||||
"drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=8:enable='between(n,{},{})'",
|
"drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=8:enable='between(n,{},{})'",
|
||||||
x, y, w, h, start_offset, end_offset - 1
|
x, y, w, h, start_offset, end_offset - 1
|
||||||
));
|
));
|
||||||
// Text: same hold behavior
|
// Text label (drawtext, 1 filter vs ~175 bitmap drawboxes)
|
||||||
let label = format!("t{}", trace_id);
|
parts.push(format!(
|
||||||
let mut tx = *x + 6;
|
"drawtext=text='{}':x={}:y={}:fontsize=20:fontcolor=white:box=1:boxcolor=red@0.8:enable='between(n,{},{})'",
|
||||||
let mut ty = *y + 6;
|
trace_id, x + 4, y + 4, start_offset, end_offset - 1
|
||||||
for ch in label.chars() {
|
));
|
||||||
let bm = bitmap_char(ch);
|
|
||||||
for (row, bits) in bm.iter().enumerate() {
|
|
||||||
for col in 0..5 {
|
|
||||||
if bits & (1 << (4 - col)) != 0 {
|
|
||||||
let dx = tx + col as i32 * 3;
|
|
||||||
let dy = ty + row as i32 * 3;
|
|
||||||
parts.push(format!(
|
|
||||||
"drawbox=x={}:y={}:w=3:h=3:color=white@1.0:t=fill:enable='between(n,{},{})'",
|
|
||||||
dx, dy, start_offset, end_offset - 1
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx += CHAR_ADVANCE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let vf = if parts.is_empty() {
|
// Write filter to temp file to bypass ARG_MAX
|
||||||
"null".to_string()
|
let filter_text = parts.join(",");
|
||||||
} else {
|
let filter_file = std::env::temp_dir().join(format!("vf_{}.txt", uuid::Uuid::new_v4()));
|
||||||
parts.join(",")
|
let _ = std::fs::write(&filter_file, &filter_text);
|
||||||
};
|
let filter_path = filter_file.to_str().unwrap_or("");
|
||||||
|
|
||||||
let tmp = std::env::temp_dir().join(format!("trace_{}.mp4", uuid::Uuid::new_v4()));
|
let tmp = std::env::temp_dir().join(format!("trace_{}.mp4", uuid::Uuid::new_v4()));
|
||||||
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
||||||
let status = ffmpeg_cmd()
|
let result = ffmpeg_cmd()
|
||||||
.args([
|
.args([
|
||||||
"-ss", &seek.to_string(), "-i", &video_path,
|
"-ss", &seek.to_string(), "-i", &video_path,
|
||||||
"-t", &duration.to_string(), "-vf", &vf,
|
"-t", &duration.to_string(),
|
||||||
|
"-/filter_complex", &filter_path,
|
||||||
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
|
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
|
||||||
"-an", "-movflags", "+faststart", "-y", &tmp_str,
|
"-an", "-movflags", "+faststart", "-y", &tmp_str,
|
||||||
])
|
])
|
||||||
.status()
|
.output()
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|e| { tracing::error!("ffmpeg spawn: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?;
|
||||||
if !status.success() {
|
if !result.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||||
|
tracing::error!("ffmpeg failed: {}", &stderr[..stderr.len().min(300)]);
|
||||||
|
let _ = std::fs::remove_file(&filter_file);
|
||||||
let _ = std::fs::remove_file(&tmp);
|
let _ = std::fs::remove_file(&tmp);
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
@@ -362,6 +344,7 @@ async fn trace_video(
|
|||||||
let data = tokio::fs::read(&tmp)
|
let data = tokio::fs::read(&tmp)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let _ = std::fs::remove_file(&filter_file);
|
||||||
let _ = std::fs::remove_file(&tmp);
|
let _ = std::fs::remove_file(&tmp);
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header(header::CONTENT_TYPE, "video/mp4")
|
.header(header::CONTENT_TYPE, "video/mp4")
|
||||||
|
|||||||
Reference in New Issue
Block a user