feat: all video endpoints support mode=normal|debug + audio=on|off

This commit is contained in:
Accusys
2026-05-14 19:04:42 +08:00
parent d2bc7c0e2d
commit 189bec929a

View File

@@ -7,9 +7,18 @@ use axum::{
Router, Router,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap;
use uuid::Uuid;
use crate::core::db::{schema, PostgresDb}; 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();
(mode, audio)
}
static FFMPEG: Lazy<String> = Lazy::new(|| { static FFMPEG: Lazy<String> = Lazy::new(|| {
std::env::var("MOMENTRY_FFMPEG").unwrap_or_else(|_| { std::env::var("MOMENTRY_FFMPEG").unwrap_or_else(|_| {
let full = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg"; let full = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg";
@@ -124,6 +133,8 @@ struct BboxParams {
end_time: Option<f64>, end_time: Option<f64>,
face_uuid: Option<String>, face_uuid: Option<String>,
duration: Option<f64>, duration: Option<f64>,
mode: Option<String>,
audio: Option<String>,
} }
/// Resolve (start_frame, end_frame) from dual input. /// Resolve (start_frame, end_frame) from dual input.
@@ -218,37 +229,28 @@ async fn bbox_overlay_video(
} }
} }
let vf = if parts.is_empty() { let bbox_mode = p.mode.as_deref().unwrap_or("normal");
let bbox_audio = p.audio.as_deref().unwrap_or("on");
let vf = if parts.is_empty() || bbox_mode == "normal" {
"null".to_string() "null".to_string()
} else { } else {
parts.join(",") parts.join(",")
}; };
let tmp = std::env::temp_dir().join(format!("bbox_{}.mp4", uuid::Uuid::new_v4())); let tmp = std::env::temp_dir().join(format!("bbox_{}.mp4", 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 ss = start_sec.to_string();
.args([ let dur = duration.to_string();
"-ss", let mut bbox_args = vec!["-ss", &ss, "-i", &video_path, "-t", &dur];
&start_sec.to_string(), if vf != "null" {
"-i", bbox_args.extend_from_slice(&["-vf", &vf, "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"]);
&video_path, } else {
"-t", bbox_args.extend_from_slice(&["-c", "copy"]);
&duration.to_string(), }
"-vf", if bbox_audio == "off" { bbox_args.push("-an"); }
&vf, bbox_args.extend_from_slice(&["-movflags", "+faststart", "-y", &tmp_str]);
"-c:v", let status = ffmpeg_cmd().args(&bbox_args).status()
"libx264",
"-preset",
"ultrafast",
"-crf",
"28",
"-an",
"-movflags",
"+faststart",
"-y",
&tmp_str,
])
.status()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !status.success() { if !status.success() {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
@@ -285,7 +287,7 @@ async fn trace_video(
) -> Result<impl IntoResponse, StatusCode> { ) -> Result<impl IntoResponse, StatusCode> {
use axum::http::header; use axum::http::header;
let mode = params.get("mode").map(|s| s.as_str()).unwrap_or("normal"); let (mode, audio) = parse_video_params(&params);
let videos_table = schema::table_name("videos"); let videos_table = schema::table_name("videos");
let row: Option<(String, f64, i32, i32)> = sqlx::query_as(&format!( let row: Option<(String, f64, i32, i32)> = sqlx::query_as(&format!(
@@ -324,12 +326,14 @@ async fn trace_video(
// === NORMAL MODE: raw video without overlays === // === NORMAL MODE: raw video without overlays ===
if mode == "normal" { if mode == "normal" {
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::new_v4()));
let tmp_str = tmp.to_str().unwrap_or("").to_string(); let tmp_str = tmp.to_str().unwrap_or("").to_string();
let result = ffmpeg_cmd() let sk = seek.to_string();
.args(["-ss", &seek.to_string(), "-i", &video_path, "-t", &duration.to_string(), let du = duration.to_string();
"-c", "copy", "-y", &tmp_str]) let mut cmd_args = vec!["-ss", &sk, "-i", &video_path, "-t", &du, "-c", "copy"];
.output() if audio == "off" { cmd_args.push("-an"); }
cmd_args.extend_from_slice(&["-y", &tmp_str]);
let result = ffmpeg_cmd().args(&cmd_args).output()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !result.status.success() { if !result.status.success() {
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
@@ -362,7 +366,6 @@ async fn trace_video(
.unwrap_or_default(); .unwrap_or_default();
// Group frames by trace_id, compute start_frame per trace; collect bbox per frame // Group frames by trace_id, compute start_frame per trace; collect bbox per frame
use std::collections::HashMap;
let mut trace_frames: HashMap<i32, Vec<i32>> = HashMap::new(); let mut trace_frames: HashMap<i32, Vec<i32>> = HashMap::new();
let mut trace_identity: HashMap<i32, String> = HashMap::new(); let mut trace_identity: HashMap<i32, String> = HashMap::new();
let mut bbox_per_frame: HashMap<(i32, i32), (i32, i32, i32, i32)> = HashMap::new(); // (tid, fn) -> (x, y, w, h) let mut bbox_per_frame: HashMap<(i32, i32), (i32, i32, i32, i32)> = HashMap::new(); // (tid, fn) -> (x, y, w, h)
@@ -453,18 +456,20 @@ async fn trace_video(
} }
let filter_text = parts.join(","); let filter_text = parts.join(",");
let filter_file = std::env::temp_dir().join(format!("vf_{}.txt", uuid::Uuid::new_v4())); let filter_file = std::env::temp_dir().join(format!("vf_{}.txt", Uuid::new_v4()));
let _ = std::fs::write(&filter_file, &filter_text); let _ = std::fs::write(&filter_file, &filter_text);
let filter_path = filter_file.to_str().unwrap_or(""); 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::new_v4()));
let tmp_str = tmp.to_str().unwrap_or("").to_string(); let tmp_str = tmp.to_str().unwrap_or("").to_string();
let result = ffmpeg_cmd() let sk = seek.to_string();
.args(["-ss", &seek.to_string(), "-i", &video_path, "-t", &duration.to_string(), let du = duration.to_string();
"-/filter_complex", &filter_path, let mut debug_args = vec!["-ss", &sk, "-i", &video_path, "-t", &du,
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "28", "-/filter_complex", &filter_path,
"-c:a", "aac", "-movflags", "+faststart", "-y", &tmp_str]) "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"];
.output() 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()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !result.status.success() { if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr); let stderr = String::from_utf8_lossy(&result.stderr);
@@ -493,6 +498,8 @@ async fn stream_video(
) -> Result<impl IntoResponse, StatusCode> { ) -> Result<impl IntoResponse, StatusCode> {
use tokio::io::{AsyncReadExt, AsyncSeekExt}; use tokio::io::{AsyncReadExt, AsyncSeekExt};
let (_mode, audio) = parse_video_params(&params);
let videos_table = schema::table_name("videos"); let videos_table = schema::table_name("videos");
let row: Option<(String,)> = sqlx::query_as(&format!( let row: Option<(String,)> = sqlx::query_as(&format!(
"SELECT file_path FROM {} WHERE file_uuid = $1", "SELECT file_path FROM {} WHERE file_uuid = $1",
@@ -537,12 +544,14 @@ async fn stream_video(
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
let tmp = std::env::temp_dir().join(format!("chunk_{}.mp4", uuid::Uuid::new_v4())); let tmp = std::env::temp_dir().join(format!("chunk_{}.mp4", 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 ss = start_sec.to_string();
.args(["-ss", &start_sec.to_string(), "-i", &file_path, "-t", &dur.to_string(), let d = dur.to_string();
"-c", "copy", "-movflags", "+faststart", "-y", &tmp_str]) let mut chunk_args = vec!["-ss", &ss, "-i", &file_path, "-t", &d, "-c", "copy"];
.status() 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()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !status.success() { if !status.success() {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);