- 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)
291 lines
7.7 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|