Files
oak-editor/crates/oak-cli/src/fmt.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

291 lines
7.7 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Output formatters matching the C++ `cli/main.cpp` byte for byte.
//!
//! The golden reference is the output of the C++ binary on the test
//! fixtures (`tests/project_with_footage.ove`, `tests/demo.mp4`), captured
//! before this crate existed. Each function below takes the plain data a
//! facade call would produce and formats it exactly like the C++ `printf`
//! call (`%.6f`, `%.3f`, `%lld`, `%d`, ...).
//!
//! The facade families that produce this data are still deferred
//! (`crate::deferred`), so the formatters are exercised by unit tests
//! against the golden text; the subcommands wire them in once the families
//! land (`dead_code` until then).
#![allow(dead_code)]
/// `Project: <name>` (`cmd_info`).
pub fn project_line(name: &str) -> String {
format!("Project: {name}")
}
/// `File: <filename>` (`cmd_info`).
pub fn file_line(filename: &str) -> String {
format!("File: {filename}")
}
/// `Modified: yes|no` (`cmd_info`).
pub fn modified_line(modified: bool) -> String {
format!("Modified: {}", if modified { "yes" } else { "no" })
}
/// `Sequences: <n>` (`cmd_info`).
pub fn sequences_line(count: i64) -> String {
format!("Sequences: {count}")
}
/// `Footage: <n>` (`cmd_info`).
pub fn footage_line(count: i64) -> String {
format!("Footage: {count}")
}
/// One sequence block (`print_sequence` in cli/main.cpp).
///
/// ```
/// [0] "Fixture Sequence"
/// length: 0.000000 s (0/1)
/// frame rate: 30000/1001 (29.970 fps)
/// tracks: video=0 audio=0 subtitle=0
/// playhead: 0 (0.000000 s)
/// ```
pub fn sequence(
index: i64,
name: &str,
length_seconds: f64,
len_num: i64,
len_den: i64,
fr_num: i64,
fr_den: i64,
video: i64,
audio: i64,
subtitle: i64,
playhead: i64,
playhead_seconds: f64,
) -> String {
let fps = if fr_den != 0 {
fr_num as f64 / fr_den as f64
} else {
0.0
};
format!(
" [{index}] \"{name}\"\n length: {length_seconds:.6} s ({len_num}/{len_den})\n \
frame rate: {fr_num}/{fr_den} ({fps:.3} fps)\n tracks: video={video} audio={audio} \
subtitle={subtitle}\n playhead: {playhead} ({playhead_seconds:.6} s)"
)
}
/// One footage entry (`cmd_info`).
///
/// ```
/// [0] "/abs/path/demo.mp4" online
/// ```
pub fn footage_entry(index: i64, filename: &str, online: bool) -> String {
format!(
" [{index}] \"{filename}\" {}",
if online { "online" } else { "offline" }
)
}
/// `Decoder: <name>` (`cmd_probe`).
pub fn decoder_line(decoder: &str) -> String {
format!("Decoder: {decoder}")
}
/// `Duration: <seconds> s` (`cmd_probe`).
pub fn duration_line(seconds: f64) -> String {
format!("Duration: {seconds:.6} s")
}
/// `Video streams: <n>` (`cmd_probe`).
pub fn video_streams_line(count: i64) -> String {
format!("Video streams: {count}")
}
/// One video-stream line (`cmd_probe`).
///
/// ```
/// [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive
/// ```
pub fn video_stream(
index: i64,
stream_index: i64,
width: i64,
height: i64,
frame_rate_num: i64,
frame_rate_den: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
color_primaries: i64,
color_trc: i64,
interlaced: bool,
) -> String {
let fps = if frame_rate_den != 0 {
frame_rate_num as f64 / frame_rate_den as f64
} else {
0.0
};
let interlace = if interlaced {
"interlaced"
} else {
"progressive"
};
format!(
" [{index}] stream {stream_index}: {width}x{height}, {frame_rate_num}/{frame_rate_den} \
fps ({fps:.3}), duration {duration_ts}/{time_base_den} ({seconds:.6} s), \
primaries={color_primaries} trc={color_trc}, {interlace}"
)
}
/// `Audio streams: <n>` (`cmd_probe`).
pub fn audio_streams_line(count: i64) -> String {
format!("Audio streams: {count}")
}
/// One audio-stream line (`cmd_probe`).
///
/// ```
/// [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)
/// ```
pub fn audio_stream(
index: i64,
stream_index: i64,
sample_rate: i64,
channel_count: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
) -> String {
format!(
" [{index}] stream {stream_index}: {sample_rate} Hz, {channel_count} channels, \
duration {duration_ts}/{time_base_den} ({seconds:.6} s)"
)
}
/// `Subtitle streams: <n>` (`cmd_probe`).
pub fn subtitle_streams_line(count: i64) -> String {
format!("Subtitle streams: {count}")
}
#[cfg(test)]
mod tests {
use super::*;
// Golden text captured from the C++ binary:
// cmake-build-debug/cli/oak-cli info tests/project_with_footage.ove
// cmake-build-debug/cli/oak-cli probe tests/demo.mp4
#[test]
fn golden_info_output() {
let mut out = String::new();
out.push_str(&project_line("project_with_footage"));
out.push('\n');
out.push_str(&file_line(
"/Users/sunyu/Projects/oak/tests/project_with_footage.ove",
));
out.push('\n');
out.push_str(&modified_line(false));
out.push('\n');
out.push_str(&sequences_line(1));
out.push('\n');
out.push_str(&sequence(
0,
"Fixture Sequence",
0.0,
0,
1,
30000,
1001,
0,
0,
0,
0,
0.0,
));
out.push('\n');
out.push_str(&footage_line(1));
out.push('\n');
out.push_str(&footage_entry(
0,
"/Users/sunyu/Projects/oak/tests/demo.mp4",
true,
));
const GOLDEN: &str = concat!(
"Project: project_with_footage\n",
"File: /Users/sunyu/Projects/oak/tests/project_with_footage.ove\n",
"Modified: no\n",
"Sequences: 1\n",
" [0] \"Fixture Sequence\"\n",
" length: 0.000000 s (0/1)\n",
" frame rate: 30000/1001 (29.970 fps)\n",
" tracks: video=0 audio=0 subtitle=0\n",
" playhead: 0 (0.000000 s)\n",
"Footage: 1\n",
" [0] \"/Users/sunyu/Projects/oak/tests/demo.mp4\" online",
);
assert_eq!(out, GOLDEN);
}
#[test]
fn golden_probe_output() {
let mut out = String::new();
out.push_str(&decoder_line("ffmpeg"));
out.push('\n');
out.push_str(&duration_line(17.0));
out.push('\n');
out.push_str(&video_streams_line(1));
out.push('\n');
out.push_str(&video_stream(
0, 0, 1920, 1080, 25, 1, 217600, 12800, 17.0, 1, 1, false,
));
out.push('\n');
out.push_str(&audio_streams_line(1));
out.push('\n');
out.push_str(&audio_stream(0, 1, 48000, 2, 816000, 48000, 17.0));
out.push('\n');
out.push_str(&subtitle_streams_line(0));
const GOLDEN: &str = concat!(
"Decoder: ffmpeg\n",
"Duration: 17.000000 s\n",
"Video streams: 1\n",
" [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive\n",
"Audio streams: 1\n",
" [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)\n",
"Subtitle streams: 0",
);
assert_eq!(out, GOLDEN);
}
#[test]
fn fps_rounding_matches_printf() {
// 30000/1001 = 29.970029... -> %.3f -> "29.970"
let s = sequence(0, "S", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0);
assert!(s.contains("frame rate: 30000/1001 (29.970 fps)"), "{s}");
}
#[test]
fn offline_footage_prints_offline() {
assert_eq!(
footage_entry(2, "gone.mp4", false),
" [2] \"gone.mp4\" offline"
);
}
}