Files
oak-editor/crates/oakengine/tests/codec.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

309 lines
10 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/>.
//! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`):
//! container/codec metadata queries and the encoding-params handle.
#[path = "common/mod.rs"]
mod common;
use std::ffi::{c_char, c_int};
use oakengine::codec::{
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
oakengine_encoding_filename_remove_digit_placeholder,
oakengine_encoding_format_audio_codec_count, oakengine_encoding_format_count,
oakengine_encoding_format_extension, oakengine_encoding_format_name,
oakengine_encoding_format_video_codec_at, oakengine_encoding_format_video_codec_count,
oakengine_encoding_generate_matrix, oakengine_encoding_image_sequence_digit_count,
oakengine_encoding_params_audio_enabled, oakengine_encoding_params_color_transform_output,
oakengine_encoding_params_create, oakengine_encoding_params_destroy,
oakengine_encoding_params_enable_audio, oakengine_encoding_params_enable_video,
oakengine_encoding_params_filename, oakengine_encoding_params_format,
oakengine_encoding_params_get_audio_params, oakengine_encoding_params_get_custom_range,
oakengine_encoding_params_get_video_params, oakengine_encoding_params_has_custom_range,
oakengine_encoding_params_is_valid, oakengine_encoding_params_set_color_transform,
oakengine_encoding_params_set_custom_range, oakengine_encoding_params_set_filename,
oakengine_encoding_params_set_format, oakengine_encoding_params_set_video_bit_rate,
oakengine_encoding_params_set_video_option, oakengine_encoding_params_set_video_pix_fmt,
oakengine_encoding_params_video_bit_rate, oakengine_encoding_params_video_codec,
oakengine_encoding_params_video_enabled, oakengine_encoding_params_video_option,
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index,
oakengine_encoding_start_audio_recording,
};
use oakengine::common::OakVideoParamsPod;
/// Container format / codec metadata queries.
#[test]
fn encoding_metadata() {
// Format enumeration: at least the six named formats exist.
let count = unsafe { oakengine_encoding_format_count() };
assert!(count >= 6);
// Matroska (1): name + extension via two-stage getters.
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_encoding_format_name(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert!(unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap()
.contains("Matroska"));
let len = unsafe { oakengine_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"mkv"
);
// Per-format codec lists.
assert!(unsafe { oakengine_encoding_format_video_codec_count(1) } >= 1);
let codec = unsafe { oakengine_encoding_format_video_codec_at(1, 0) };
assert!(codec >= 1);
assert!(unsafe { oakengine_encoding_format_audio_codec_count(1) } >= 1);
// Codec metadata: name, still-image (PNG = 5), lossless.
let len = unsafe { oakengine_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
assert!(len > 0);
assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(5) }, 1); // PNG
assert_eq!(unsafe { oakengine_encoding_codec_is_still_image(1) }, 0); // H264
assert!(unsafe { oakengine_encoding_codec_is_lossless(13) } == 1); // PCM
// pix_fmt_index: preferred format index when absent.
assert_eq!(
unsafe { oakengine_encoding_pix_fmt_index(1, c"yuv420p".as_ptr()) },
0
);
}
/// Image-sequence filename helpers.
#[test]
fn filename_helpers() {
assert_eq!(
unsafe {
oakengine_encoding_filename_contains_digit_placeholder(c"img[#####].png".as_ptr())
},
1
);
assert_eq!(
unsafe { oakengine_encoding_filename_contains_digit_placeholder(c"img.png".as_ptr()) },
0
);
assert_eq!(
unsafe { oakengine_encoding_image_sequence_digit_count(c"img[#####].png".as_ptr()) },
5
);
let mut buf = [0 as c_char; 64];
let len = unsafe {
oakengine_encoding_filename_remove_digit_placeholder(
c"img[#####].png".as_ptr(),
buf.as_mut_ptr(),
64,
)
};
assert!(len > 0);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"img.png"
);
}
/// Transform matrix: fit produces a valid 16-float matrix.
#[test]
fn generate_matrix() {
let mut m = [0.0_f32; 16];
assert_eq!(
unsafe { oakengine_encoding_generate_matrix(0, 1920, 1080, 960, 540, m.as_mut_ptr()) },
0
);
// The 4x4 identity-ish matrix has a non-zero top-left.
assert!(m[0] > 0.0 || m[5] > 0.0);
// NULL output → E_INVALID.
assert_eq!(
unsafe { oakengine_encoding_generate_matrix(0, 1, 1, 1, 1, std::ptr::null_mut()) },
-1
);
}
/// Encoding-params handle lifecycle: create → configure → read back →
/// destroy (serialized inside one test; the handle is per-call state).
#[test]
fn params_handle_round_trip() {
let p = unsafe { oakengine_encoding_params_create() };
assert!(!p.is_null());
// Fresh: nothing enabled, format unset (-1).
assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 0);
assert_eq!(unsafe { oakengine_encoding_params_format(p) }, -1);
// Format: set + get, and reject out-of-range.
assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 1) }, 0); // Matroska
assert_eq!(unsafe { oakengine_encoding_params_format(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_set_format(p, 9999) }, -1);
// Filename round-trip.
assert_eq!(
unsafe { oakengine_encoding_params_set_filename(p, c"out.mkv".as_ptr()) },
0
);
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_encoding_params_filename(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 7);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"out.mkv"
);
// Enable video: valid, get_video_params reads back.
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe {
oakengine::common::oakengine_video_params_make(
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
)
},
0
);
assert_eq!(
unsafe { oakengine_encoding_params_enable_video(p, &vp, 1) },
0
);
assert_eq!(unsafe { oakengine_encoding_params_is_valid(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_video_enabled(p) }, 1);
assert_eq!(unsafe { oakengine_encoding_params_video_codec(p) }, 1);
let mut out_vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_encoding_params_get_video_params(p, &mut out_vp) },
0
);
assert_eq!(out_vp.width, 1920);
assert_eq!(out_vp.height, 1080);
assert_eq!(out_vp.time_base_num, 1001);
// Video bit rate round-trip.
unsafe { oakengine_encoding_params_set_video_bit_rate(p, 8_000_000) };
assert_eq!(
unsafe { oakengine_encoding_params_video_bit_rate(p) },
8_000_000
);
// Encoded pixel format round-trip.
assert_eq!(
unsafe { oakengine_encoding_params_set_video_pix_fmt(p, c"yuv420p".as_ptr()) },
0
);
let len = unsafe { oakengine_encoding_params_video_pix_fmt(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 7);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"yuv420p"
);
// Audio: disabled get_video/audio → E_STATE; enable then read back.
let mut sr: c_int = 0;
let mut layout: u64 = 0;
let mut sf: c_int = 0;
assert_eq!(
unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) },
-2
);
assert_eq!(
unsafe { oakengine_encoding_params_enable_audio(p, 48000, 3, 0, 13) },
0
);
assert_eq!(unsafe { oakengine_encoding_params_audio_enabled(p) }, 1);
assert_eq!(
unsafe { oakengine_encoding_params_get_audio_params(p, &mut sr, &mut layout, &mut sf) },
0
);
assert_eq!(sr, 48000);
assert_eq!(layout, 3);
// Custom range: not set → E_NOT_FOUND; set → reads back.
let (mut inn, mut ind, mut outn, mut outd) = (0i64, 0i64, 0i64, 0i64);
assert_eq!(
unsafe {
oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd)
},
-4
);
unsafe { oakengine_encoding_params_set_custom_range(p, 0, 1, 100, 1) };
assert_eq!(unsafe { oakengine_encoding_params_has_custom_range(p) }, 1);
assert_eq!(
unsafe {
oakengine_encoding_params_get_custom_range(p, &mut inn, &mut ind, &mut outn, &mut outd)
},
0
);
assert_eq!((inn, ind, outn, outd), (0, 1, 100, 1));
// Color transform + video option round-trips.
assert_eq!(
unsafe { oakengine_encoding_params_set_color_transform(p, c"ACEScg".as_ptr()) },
0
);
let len = unsafe { oakengine_encoding_params_color_transform_output(p, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"ACEScg"
);
assert_eq!(
unsafe { oakengine_encoding_params_set_video_option(p, c"crf".as_ptr(), c"18".as_ptr()) },
0
);
let len =
unsafe { oakengine_encoding_params_video_option(p, c"crf".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 2);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_str()
.unwrap(),
"18"
);
assert_eq!(
unsafe {
oakengine_encoding_params_video_option(p, c"missing".as_ptr(), buf.as_mut_ptr(), 64)
},
-4
);
unsafe { oakengine_encoding_params_destroy(p) };
// NULL destroy is a no-op.
unsafe { oakengine_encoding_params_destroy(std::ptr::null_mut()) };
}
/// Audio recording without a running audio manager fails with E_STATE.
#[test]
fn start_audio_recording_no_manager() {
let p = unsafe { oakengine_encoding_params_create() };
assert!(!p.is_null());
let rc = unsafe { oakengine_encoding_start_audio_recording(p, std::ptr::null_mut(), 0) };
assert_eq!(rc, -2); // OAKENGINE_E_STATE
unsafe { oakengine_encoding_params_destroy(p) };
}