From 283da8e767f409e3afdcb40f500cbd30cd50197a Mon Sep 17 00:00:00 2001 From: Accusys Date: Fri, 8 May 2026 14:03:30 +0800 Subject: [PATCH] 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 --- src/api/media_api.rs | 63 ++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/src/api/media_api.rs b/src/api/media_api.rs index fe36641..bde14d2 100644 --- a/src/api/media_api.rs +++ b/src/api/media_api.rs @@ -297,64 +297,46 @@ async fn trace_video( let duration = (last_frame - first_frame) as f64 / fps + padding * 2.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 = Vec::new(); 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 { - // For last detection, extend to duration end - last_frame + (padding * fps) as i32 - }; + let next_frame = if i + 1 < rows.len() { rows[i + 1].0 } else { last_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; - - // Bbox: visible from this frame until next detection + // Bbox parts.push(format!( "drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=8:enable='between(n,{},{})'", x, y, w, h, start_offset, end_offset - 1 )); - // Text: same hold behavior - let label = format!("t{}", trace_id); - let mut tx = *x + 6; - let mut ty = *y + 6; - 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; - } + // Text label (drawtext, 1 filter vs ~175 bitmap drawboxes) + parts.push(format!( + "drawtext=text='{}':x={}:y={}:fontsize=20:fontcolor=white:box=1:boxcolor=red@0.8:enable='between(n,{},{})'", + trace_id, x + 4, y + 4, start_offset, end_offset - 1 + )); } - let vf = if parts.is_empty() { - "null".to_string() - } else { - parts.join(",") - }; + // Write filter to temp file to bypass ARG_MAX + let filter_text = parts.join(","); + let filter_file = std::env::temp_dir().join(format!("vf_{}.txt", uuid::Uuid::new_v4())); + 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_str = tmp.to_str().unwrap_or("").to_string(); - let status = ffmpeg_cmd() + let result = ffmpeg_cmd() .args([ "-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", "-an", "-movflags", "+faststart", "-y", &tmp_str, ]) - .status() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - if !status.success() { + .output() + .map_err(|e| { tracing::error!("ffmpeg spawn: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; + 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); return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -362,6 +344,7 @@ async fn trace_video( 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() .header(header::CONTENT_TYPE, "video/mp4")