Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/reco-cli/src/stitch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,29 @@ pub fn run_stitch(args: StitchArgs<'_>, interrupted: &Arc<AtomicBool>) -> anyhow
reco_io::stitch_job::InputPath::Single(std::path::PathBuf::from(s))
}
};
// Snapshot the settings actually used for this export into the
// output container's "comment" tag, so a batch of test exports
// with varying AI/blend settings stays self-describing without a
// separate sidecar file (`ffprobe -show_entries format_tags`).
let metadata_comment = serde_json::json!({
"reco_export": {
"codec": args.codec,
"quality": args.quality,
"quality_value": args.quality_value,
"resolution": format!("{}x{}", args.width, args.height),
"blend_width": args.blend,
"autocam": {
"model_path": args.model_path,
"tracking_mode": args.tracking_mode,
"detection_interval": args.detection_interval,
"lookahead_secs": args.lookahead,
"panner_preset": args.panner_preset,
"panner_config_path": args.panner_config_path,
}
}
})
.to_string();

let mut job = reco_io::StitchJob::with_calibration(
to_input(args.left),
to_input(args.right),
Expand All @@ -116,6 +139,7 @@ pub fn run_stitch(args: StitchArgs<'_>, interrupted: &Arc<AtomicBool>) -> anyhow
.codec(parse_codec(args.codec))
.quality(parse_quality(args.quality))
.resolution(args.width, args.height)
.metadata_comment(metadata_comment)
.on_progress(move |p: &reco_core::session::types::FrameProgress| {
// Use the session's own elapsed clock so the reported
// rate excludes one-time GPU / encoder / ORT init and
Expand Down
32 changes: 32 additions & 0 deletions crates/reco-gui/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,37 @@ pub fn run_export(
log::info!("Streaming to {}", effective_output.display());
}

// Snapshot the settings actually used for this export into the
// output container's "comment" tag, so a batch of test exports
// with varying AI/blend settings stays self-describing without a
// separate sidecar file (`ffprobe -show_entries format_tags`).
let metadata_comment = serde_json::json!({
"reco_export": {
"codec": codec_str,
"quality": quality_str,
"resolution": format!("{width}x{height}"),
"blend_width": blend,
"autocam": {
"enabled": autocam.enabled,
"model_path": &autocam.model_path,
"tracking_mode": &autocam.tracking_mode,
"detection_interval": autocam.detection_interval,
"lookahead_secs": autocam.lookahead_secs,
"preset": &autocam.preset,
"framing": &autocam.framing,
"lock_pitch": autocam.lock_pitch,
"cluster_mode": &autocam.cluster_mode,
"cluster_bandwidth_rad": autocam.cluster_bandwidth_rad,
"dead_zone_rad": autocam.dead_zone_rad,
"ball_weight": autocam.ball_weight,
"fov_tight": autocam.fov_tight,
"fov_wide": autocam.fov_wide,
"fov_default": autocam.fov_default,
}
}
})
.to_string();

let mut job = reco_io::StitchJob::with_calibration(
left.clone(),
right.clone(),
Expand All @@ -196,6 +227,7 @@ pub fn run_export(
.format(format)
.resolution(width, height)
.blend_width(blend)
.metadata_comment(metadata_comment)
.on_progress(move |p: &reco_core::session::types::FrameProgress| {
let frames = p.frames_completed;
let elapsed = progress_start.elapsed().as_secs_f64();
Expand Down
1 change: 1 addition & 0 deletions crates/reco-io/src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ pub fn create_encoder(
container: ffmpeg::encoder::Container::default(),
gop_size: None,
stream_url: None,
metadata_comment: None,
};
let encoder = FfmpegFileEncoder::new(path, width, height, fps, &enc_config)?;
let name = encoder.encoder_name().to_string();
Expand Down
16 changes: 16 additions & 0 deletions crates/reco-io/src/ffmpeg/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ pub struct EncoderConfig {
/// output file AND this RTMP endpoint. Single encode pass, zero
/// extra CPU. Stream failures are non-fatal (recording continues).
pub stream_url: Option<String>,
/// Free-form text written to the output container's "comment"
/// metadata tag (visible via `ffprobe`/most media tools). Intended
/// for a JSON snapshot of the export/AI settings actually used, so
/// test exports remain self-describing. `None` writes no tag.
pub metadata_comment: Option<String>,
}

type OpenedVideoEncoder = (
Expand Down Expand Up @@ -691,6 +696,17 @@ impl VideoEncoder {
None
};

// Container metadata must be set before
// write_header - the muxer bakes it into the
// header (moov `udta` atom for MP4, Matroska's
// Tags element, etc.) and won't pick up later
// changes.
if let Some(ref comment) = config.metadata_comment {
let mut meta = ffmpeg::Dictionary::new();
meta.set("comment", comment);
octx.set_metadata(meta);
}

// Fragmented MP4 needs `movflags` so the muxer
// writes an `empty_moov` up front and flushes
// self-contained fragments on every keyframe.
Expand Down
1 change: 1 addition & 0 deletions crates/reco-io/src/stacked_video/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl Default for StackedEncoderConfig {
// default of 250.
gop_size: Some(30),
stream_url: None,
metadata_comment: None,
},
fps: (30, 1),
}
Expand Down
16 changes: 16 additions & 0 deletions crates/reco-io/src/stitch_job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ pub struct StitchJob {
/// `JsonlSink` to the session that records every detection,
/// filter decision, and pan decision for offline analysis.
events_path: Option<std::path::PathBuf>,

/// Free-form text embedded in the output container's "comment"
/// metadata tag. See [`Self::metadata_comment`].
metadata_comment: Option<String>,
}

/// Configuration for optional replay recording (see
Expand Down Expand Up @@ -258,6 +262,7 @@ impl StitchJob {
force_cpu_decode: false,
lookahead_secs: 0.0,
events_path: None,
metadata_comment: None,
}
}

Expand Down Expand Up @@ -305,6 +310,16 @@ impl StitchJob {
self
}

/// Embed free-form text (e.g. a JSON snapshot of the export/AI
/// settings used) in the output container's "comment" metadata tag,
/// readable via `ffprobe -show_entries format_tags` or most media
/// tools, so a batch of test exports stays self-describing without
/// a separate sidecar file. `None` (default) writes no tag.
pub fn metadata_comment(mut self, comment: impl Into<String>) -> Self {
self.metadata_comment = Some(comment.into());
self
}

/// Set the audio mode. Default: copy audio from the first input.
pub fn audio(mut self, mode: AudioMode) -> Self {
self.audio = mode;
Expand Down Expand Up @@ -697,6 +712,7 @@ impl StitchJob {
container: self.format.into(),
gop_size: None,
stream_url: None,
metadata_comment: self.metadata_comment.clone(),
};
let encoder = crate::adapters::FfmpegFileEncoder::new(
&self.output,
Expand Down
Loading