feat: dual input (start_frame/end_frame + start_time/end_time) + all outputs include frames, time, fps
This commit is contained in:
@@ -114,12 +114,36 @@ fn render_text(
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct BboxParams {
|
||||
// Legacy (deprecated): single param, frames
|
||||
start: Option<i32>,
|
||||
end: Option<i32>,
|
||||
// Explicit params: input either or both
|
||||
start_frame: Option<i32>,
|
||||
end_frame: Option<i32>,
|
||||
start_time: Option<f64>,
|
||||
end_time: Option<f64>,
|
||||
face_uuid: Option<String>,
|
||||
duration: Option<f64>,
|
||||
}
|
||||
|
||||
/// Resolve (start_frame, end_frame) from dual input.
|
||||
/// 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>,
|
||||
fps: f64,
|
||||
) -> (i32, i32) {
|
||||
if let (Some(sf), Some(ef)) = (start_frame.or(start), end_frame.or(end)) {
|
||||
return (sf, ef);
|
||||
}
|
||||
if let (Some(st), Some(et)) = (start_time, end_time) {
|
||||
return ((st * fps) as i32, (et * fps) as i32);
|
||||
}
|
||||
(0, i32::MAX)
|
||||
}
|
||||
|
||||
async fn bbox_overlay_video(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(file_uuid): Path<String>,
|
||||
@@ -136,8 +160,6 @@ async fn bbox_overlay_video(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let (video_path,) = row.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
let start_f = p.start.unwrap_or(0);
|
||||
let end_f = p.end.unwrap_or(i32::MAX);
|
||||
let face_fuid = p.face_uuid.as_deref().unwrap_or(&file_uuid);
|
||||
let duration = p.duration.unwrap_or(10.0);
|
||||
|
||||
@@ -152,6 +174,8 @@ 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_sec = start_f as f64 / fps;
|
||||
|
||||
// Get face bboxes
|
||||
@@ -487,11 +511,30 @@ async fn stream_video(
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Chunk extraction with start/end params
|
||||
if let (Some(s), Some(e)) = (params.get("start"), params.get("end")) {
|
||||
let start: f64 = s.parse().unwrap_or(0.0);
|
||||
let end: f64 = e.parse().unwrap_or(0.0);
|
||||
let dur = end - start;
|
||||
// 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 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();
|
||||
|
||||
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);
|
||||
(sf / _fps, (ef - sf) / _fps)
|
||||
} else if let (Some(st), Some(et)) = (start_time_param, end_time_param) {
|
||||
(st, et - st)
|
||||
} else if let (Some(s), Some(e)) = (start_legacy, end_legacy) {
|
||||
(s, e - s)
|
||||
} else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
if dur <= 0.0 {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
@@ -499,29 +542,15 @@ async fn stream_video(
|
||||
let tmp = std::env::temp_dir().join(format!("chunk_{}.mp4", uuid::Uuid::new_v4()));
|
||||
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
||||
let status = ffmpeg_cmd()
|
||||
.args([
|
||||
"-ss",
|
||||
&start.to_string(),
|
||||
"-i",
|
||||
&file_path,
|
||||
"-t",
|
||||
&dur.to_string(),
|
||||
"-c",
|
||||
"copy",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"-y",
|
||||
&tmp_str,
|
||||
])
|
||||
.args(["-ss", &start_sec.to_string(), "-i", &file_path, "-t", &dur.to_string(),
|
||||
"-c", "copy", "-movflags", "+faststart", "-y", &tmp_str])
|
||||
.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")
|
||||
@@ -530,6 +559,7 @@ async fn stream_video(
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
// Full file streaming with range request support
|
||||
let file_size = src.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
let content_type = "video/mp4";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user