// 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 `