// 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 .
//! `olive::EncodingParams` — export encoding parameters.
//!
//! Mirrors the `EncodingParams` class carried in `src/codec/src/encoder.h`
//! and the flattened `oakcodec_encoding_params` POD in
//! `include/codec/encoder.h`. The Rust struct mirrors the POD verbatim so
//! `ffi.rs` can marshal it without translation.
use oakcore_rs::{PixelFormat, SampleFormat};
/// `VideoParams::Interlacing` values.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum VideoScalingMethod {
/// Fit the source into the destination, preserving aspect.
Fit = 0,
/// Stretch the source to the destination.
Stretch = 1,
/// Crop the source to the destination.
Crop = 2,
}
/// `olive::EncodingParams` — flattened mirror of `oakcodec_encoding_params`.
#[derive(Clone, Debug)]
#[repr(C)]
pub struct EncodingParams {
/// Output filename (or image-sequence "[#####]" template).
pub filename: [u8; 1024],
/// `ExportFormat::Format`.
pub format: i32,
/// Video track enabled (1/0; C `int`).
pub video_enabled: i32,
/// `ExportCodec::Codec`.
pub video_codec: i32,
/// Output width.
pub video_width: i32,
/// Output height.
pub video_height: i32,
/// Frame duration (rational): numerator.
pub video_time_base_num: i32,
/// Frame duration (rational): denominator.
pub video_time_base_den: i32,
/// Delivery `OakPixelFormat`.
pub video_pixel_format: PixelFormat,
/// Interlacing (`Interlacing` value).
pub video_interlacing: i32,
/// Pixel aspect ratio numerator.
pub video_pixel_aspect_num: i32,
/// Pixel aspect ratio denominator.
pub video_pixel_aspect_den: i32,
/// Bit rate (bit/s), 0 = codec default.
pub video_bit_rate: i64,
/// Min bit rate.
pub video_min_bit_rate: i64,
/// Max bit rate.
pub video_max_bit_rate: i64,
/// Buffer size (bytes).
pub video_buffer_size: i64,
/// Encoding threads (0 = auto).
pub video_threads: i32,
/// Encoded pixel format name (e.g. "yuv420p").
pub video_pix_fmt: [u8; 64],
/// Image sequence output (1/0).
pub video_is_image_sequence: i32,
/// Scaling method (`VideoScalingMethod`).
pub video_scaling_method: VideoScalingMethod,
/// Audio track enabled (1/0).
pub audio_enabled: i32,
/// `ExportCodec::Codec`.
pub audio_codec: i32,
/// Audio sample rate.
pub audio_sample_rate: i32,
/// ffmpeg-style channel layout mask.
pub audio_channel_layout: u64,
/// `SampleFormat`.
pub audio_sample_format: SampleFormat,
/// Audio bit rate.
pub audio_bit_rate: i64,
/// Subtitles track enabled (1/0).
pub subtitles_enabled: i32,
/// `ExportCodec::Codec`.
pub subtitles_codec: i32,
/// Subtitles written as a sidecar file (1/0).
pub subtitles_are_sidecar: i32,
/// Sidecar format (`ExportFormat::Format`).
pub subtitles_sidecar_format: i32,
/// Output OCIO colorspace name; empty = reference space (no transform).
pub color_transform_output: [u8; 256],
/// Export length in seconds (rational).
pub export_length_num: i32,
/// Export length in seconds (rational).
pub export_length_den: i32,
/// Custom export range enabled (1/0).
pub has_custom_range: i32,
/// Custom range in, rational seconds.
pub custom_range_in_num: i64,
/// Custom range in denominator.
pub custom_range_in_den: i64,
/// Custom range out, rational seconds.
pub custom_range_out_num: i64,
/// Custom range out denominator.
pub custom_range_out_den: i64,
}
impl Default for EncodingParams {
fn default() -> Self {
Self {
filename: [0; 1024],
format: -1,
video_enabled: 0,
video_codec: -1,
video_width: 0,
video_height: 0,
video_time_base_num: 1,
video_time_base_den: 1,
video_pixel_format: PixelFormat::Invalid,
video_interlacing: 0,
video_pixel_aspect_num: 0,
video_pixel_aspect_den: 0,
video_bit_rate: 0,
video_min_bit_rate: 0,
video_max_bit_rate: 0,
video_buffer_size: 0,
video_threads: 0,
video_pix_fmt: [0; 64],
video_is_image_sequence: 0,
video_scaling_method: VideoScalingMethod::Stretch,
audio_enabled: 0,
audio_codec: -1,
audio_sample_rate: 0,
audio_channel_layout: 0,
audio_sample_format: SampleFormat::Invalid,
audio_bit_rate: 0,
subtitles_enabled: 0,
subtitles_codec: -1,
subtitles_are_sidecar: 0,
subtitles_sidecar_format: -1,
color_transform_output: [0; 256],
export_length_num: 0,
export_length_den: 0,
has_custom_range: 0,
custom_range_in_num: 0,
custom_range_in_den: 0,
custom_range_out_num: 0,
custom_range_out_den: 0,
}
}
}
impl EncodingParams {
/// Scaling matrix for a scaling method; row-major 4x4 into `out[16]`.
///
/// # CPP-PARITY
/// `src/codec/src/encoder.cpp` `EncodingParams::generate_matrix` —
/// returns a row-major `std::array` (formerly QMatrix4x4).
pub fn generate_matrix(
method: VideoScalingMethod,
src_width: i32,
src_height: i32,
dst_width: i32,
dst_height: i32,
out: &mut [f64; 16],
) {
// Identity (former default-constructed QMatrix4x4), row-major.
*out = [
1.0, 0.0, 0.0, 0.0, //
0.0, 1.0, 0.0, 0.0, //
0.0, 0.0, 1.0, 0.0, //
0.0, 0.0, 0.0, 1.0,
];
if method == VideoScalingMethod::Stretch {
return;
}
// Guard degenerate sizes: the C++ would produce inf/NaN here; an
// identity is the only safe output for the rendering pipeline.
if src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 {
return;
}
let export_ar = dst_width as f64 / dst_height as f64;
let source_ar = src_width as f64 / src_height as f64;
// qFuzzyCompare(export_ar, source_ar): within one part in 100000.
if (export_ar - source_ar).abs() * 100000.0 <= export_ar.abs().min(source_ar.abs()) {
return;
}
if (export_ar > source_ar) == (method == VideoScalingMethod::Fit) {
// scale(source_ar / export_ar, 1)
out[0] = source_ar / export_ar;
} else {
// scale(1, export_ar / source_ar)
out[5] = export_ar / source_ar;
}
}
/// File extension for this format (e.g. "mp4").
///
/// # CPP-PARITY
/// `ExportFormat::get_extension` (`src/codec/src/exportformat.cpp`) keyed
/// on the serialized `Format` discriminant. Unknown / invalid formats
/// return the empty string, matching the C++ default case.
pub fn extension(&self) -> &str {
match self.format {
0 => "mxf", // DNxHD
1 => "mkv", // Matroska
2 => "mp4", // MPEG-4 video
3 => "exr", // OpenEXR
4 => "mov", // QuickTime
5 => "png", // PNG
6 => "tiff", // TIFF
7 => "wav", // WAV
8 => "aiff", // AIFF
9 => "mp3", // MP3
10 => "flac", // FLAC
11 => "ogg", // Ogg
12 => "webm", // WebM
13 => "srt", // SRT
14 => "m4a", // MPEG-4 audio
_ => "",
}
}
/// Load from a compact XML preset string (oakcommon C++ XmlStreamReader).
///
/// # CPP-PARITY
/// `EncodingParams::load` — uses oakcommon's C++ `XmlStreamReader`
/// (`src/common/src/xmlutils.h`), a C++-to-C++ coupling the bridge
/// cannot cover (NOTES.md §7). Preserves the load_v1 bug of not
/// assigning `custom_range`.
pub fn load(&mut self, data: &str) -> crate::error::Result<()> {
let root = parse_preset(data)
.map_err(|e| crate::error::Error::Failed(format!("invalid export preset: {e}")))?;
if root.name != "export" {
return Err(crate::error::Error::Failed(
"not an export preset document".to_string(),
));
}
// `range` / `customrangein` / `customrangeout` are parsed for shape
// parity but never assigned — mirroring the C++ `load_v1` bug that
// reads them into locals and forgets to store `custom_range`.
if let Some(e) = child(&root, "range") {
let _ = parse_bool(&text(e));
}
if let Some(e) = child(&root, "customrangein") {
let _ = &text(e);
}
if let Some(e) = child(&root, "customrangeout") {
let _ = &text(e);
}
if let Some(e) = child(&root, "filename") {
set_cstr(&mut self.filename, &text(e));
}
if let Some(e) = child(&root, "format") {
self.format = parse_i32(&text(e));
}
if let Some(e) = child(&root, "video") {
if let Some(v) = attr(e, "enabled") {
self.video_enabled = parse_bool(v) as i32;
}
if let Some(e) = child(e, "codec") {
self.video_codec = parse_i32(&text(e));
}
if let Some(e) = child(e, "width") {
self.video_width = parse_i32(&text(e));
}
if let Some(e) = child(e, "height") {
self.video_height = parse_i32(&text(e));
}
if let Some(e) = child(e, "format") {
self.video_pixel_format = match parse_i32(&text(e)) {
-1 => PixelFormat::Invalid,
0 => PixelFormat::U8,
1 => PixelFormat::U10,
2 => PixelFormat::U16,
3 => PixelFormat::F16,
4 => PixelFormat::F32,
_ => PixelFormat::Invalid,
};
}
if let Some(e) = child(e, "timebase") {
let tb = text(e);
if let Some((n, d)) = tb.split_once('/') {
self.video_time_base_num = parse_i32(n);
self.video_time_base_den = parse_i32(d);
}
}
if let Some(e) = child(e, "divider") {
self.video_interlacing = parse_i32(&text(e));
}
if let Some(e) = child(e, "pixelaspect") {
let par = text(e);
if let Some((n, d)) = par.split_once('/') {
self.video_pixel_aspect_num = parse_i32(n);
self.video_pixel_aspect_den = parse_i32(d);
}
}
if let Some(e) = child(e, "bitrate") {
self.video_bit_rate = parse_i64(&text(e));
}
if let Some(e) = child(e, "minbitrate") {
self.video_min_bit_rate = parse_i64(&text(e));
}
if let Some(e) = child(e, "maxbitrate") {
self.video_max_bit_rate = parse_i64(&text(e));
}
if let Some(e) = child(e, "bufsize") {
self.video_buffer_size = parse_i64(&text(e));
}
if let Some(e) = child(e, "threads") {
self.video_threads = parse_i32(&text(e));
}
if let Some(e) = child(e, "pixfmt") {
set_cstr(&mut self.video_pix_fmt, &text(e));
}
if let Some(e) = child(e, "imgseq") {
self.video_is_image_sequence = parse_bool(&text(e)) as i32;
}
if let Some(e) = child(e, "vscale") {
self.video_scaling_method = scaling_from_i32(parse_i32(&text(e)));
}
}
if let Some(e) = child(&root, "audio") {
if let Some(v) = attr(e, "enabled") {
self.audio_enabled = parse_bool(v) as i32;
}
if let Some(e) = child(e, "codec") {
self.audio_codec = parse_i32(&text(e));
}
if let Some(e) = child(e, "samplerate") {
self.audio_sample_rate = parse_i32(&text(e));
}
if let Some(e) = child(e, "channellayout") {
self.audio_channel_layout = parse_i64(&text(e)) as u64;
}
if let Some(e) = child(e, "format") {
self.audio_sample_format = sample_format_from_i32(parse_i32(&text(e)));
}
if let Some(e) = child(e, "bitrate") {
self.audio_bit_rate = parse_i64(&text(e));
}
}
if let Some(e) = child(&root, "subtitles") {
if let Some(v) = attr(e, "enabled") {
self.subtitles_enabled = parse_bool(v) as i32;
}
if let Some(e) = child(e, "sidecar") {
self.subtitles_are_sidecar = parse_bool(&text(e)) as i32;
}
if let Some(e) = child(e, "sidecarformat") {
self.subtitles_sidecar_format = parse_i32(&text(e));
}
if let Some(e) = child(e, "codec") {
self.subtitles_codec = parse_i32(&text(e));
}
}
Ok(())
}
/// Serialize to a compact XML string (no declaration, no indentation;
/// element/attribute names and order preserved).
///
/// # CPP-PARITY
/// The C++ writer also emits per-codec `video_opts_`; those moved out
/// of the byte-exact `EncodingParams` POD (see `encoding_params_c_abi_layout`),
/// so the options are not serialized here.
pub fn save_to_string(&self) -> String {
let mut s = String::new();
s.push_str("");
s.push_str(&format!(
"{}",
escape_xml(cstr(&self.filename))
));
s.push_str(&format!("{}", self.format));
s.push_str(&format!("{}", self.has_custom_range));
s.push_str(&format!(
"{}/{}",
self.custom_range_in_num, self.custom_range_in_den
));
s.push_str(&format!(
"{}/{}",
self.custom_range_out_num, self.custom_range_out_den
));
s.push_str(&format!("");
s.push_str(&format!("");
s.push_str(&format!(
"",
self.subtitles_enabled
));
if self.subtitles_enabled != 0 {
s.push_str(&format!(
"{}",
self.subtitles_are_sidecar
));
s.push_str(&format!(
"{}",
self.subtitles_sidecar_format
));
s.push_str(&format!("{}", self.subtitles_codec));
}
s.push_str("");
s.push_str("");
s
}
}
// ---------------------------------------------------------------------------
// Minimal XML helpers for the round-trip `load`/`save_to_string`.
//
// CPP-PARITY: the C++ `load`/`save_to_string` go through oakcommon's
// `XmlStreamReader`/`XmlStreamWriter` (a C++-to-C++ coupling the Rust bridge
// cannot cover, NOTES.md §7). Rather than returning `Err`, this port keeps a
// minimal but faithful round-trip for the fields representable without the
// videoparams/colortransform/audio bridge. Deviations vs the C++ writer:
// * the `