// 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 . //! Sequence: the C++ `ViewerOutput`/`Sequence` pair — a node that owns //! tracks, markers, work area, and playback caches. //! `// CPP-PARITY: src/node/src/output/viewer/viewer.{h,cpp}`, //! `// CPP-PARITY: src/node/src/project/sequence/sequence.{h,cpp}`. use crate::id::NodeId; use crate::input::Input; use crate::node::{Category, NodeBehavior, NodeCore}; use crate::value::{AudioParams, NodeValue, ValueType, VideoParams}; /// Sequence texture/samples input ids (ViewerOutput::k_texture_input / /// k_samples_input) and the track input id format (Sequence:: /// k_track_input_format). pub const TEXTURE_INPUT: &str = "tex_in"; pub const SAMPLES_INPUT: &str = "samples_in"; pub const TRACK_INPUT_FORMAT: &str = "track_in_%1"; /// Sequence behavior (viewer node). pub struct SequenceBehavior { /// Track list node ids (video then audio, C++ order). pub track_lists: Vec, /// Timeline markers handle (oaktimeline, owned; created lazily by /// the facade through the C ABI). pub markers: crate::handle::CHandle, /// Work area handle (oaktimeline, owned; created lazily by the /// facade through the C ABI). pub workarea: crate::handle::CHandle, /// Length cache (C++ last_length_). pub last_length: oakcore_rs::Rational, /// Autocache toggles. pub autocache_video: bool, /// Audio autocache toggle. pub autocache_audio: bool, /// Playhead position (C++ ViewerOutput::playhead_). pub playhead: oakcore_rs::Rational, /// Video parameter streams. pub video_params: Vec, /// Audio parameter streams. pub audio_params: Vec, } impl SequenceBehavior { /// Empty sequence with zero tracks and no parameters. pub fn new() -> Self { SequenceBehavior { track_lists: Vec::new(), markers: crate::handle::CHandle::null(), workarea: crate::handle::CHandle::null(), last_length: oakcore_rs::Rational::new(0, 1), autocache_video: false, autocache_audio: false, playhead: oakcore_rs::Rational::new(0, 1), video_params: Vec::new(), audio_params: Vec::new(), } } /// Constructor: the C++ `Sequence` input surface with default /// parameters but no track lists (`// CPP-PARITY: sequence.cpp:36`, /// `viewer.cpp:84`). Used by the serializer to rebuild a sequence /// from a file — the track lists arrive as separate nodes. pub fn create() -> (NodeCore, Box) { let mut core = NodeCore::new(); // Bin item (C++ `sequence.cpp:37` `set_flag(k_is_item)`): nested // sequences are shared, never cloned, by dependency-graph copies. core.flags |= crate::node::flags::IS_ITEM; // Viewer parameter streams (C++ ViewerOutput::kVideoParamsInput / // kAudioParamsInput / kSubtitleParamsInput arrays). for (id, ty) in [ ("video_param_in", ValueType::VideoParams), ("audio_param_in", ValueType::AudioParams), ("subtitle_param_in", ValueType::None), ] { let mut input = Input::new(id, ty, NodeValue::None); input.flags |= crate::input::flags::NOT_CONNECTABLE | crate::input::flags::NOT_KEYFRAMABLE | crate::input::flags::ARRAY | crate::input::flags::HIDDEN; core.add_input(input); } core.add_input(Input::new( TEXTURE_INPUT, ValueType::Texture, NodeValue::None, )); core.add_input(Input::new( SAMPLES_INPUT, ValueType::Samples, NodeValue::None, )); // One array input per track list (`track_in_%1`; the video list // owns track_in_0, audio track_in_1, subtitle track_in_2 — // `// CPP-PARITY: sequence.h`). for base in 0..3 { let mut track_input = Input::new( &TRACK_INPUT_FORMAT.replace("%1", &base.to_string()), ValueType::None, NodeValue::None, ); track_input.flags |= crate::input::flags::ARRAY; core.add_input(track_input); } let mut behavior = SequenceBehavior::new(); behavior.set_default_parameters(); (core, Box::new(behavior)) } /// Apply the default video/audio parameters (C++ /// `ViewerOutput::set_default_parameters()`; the config lookups read /// the oakcommon config store directly). pub fn set_default_parameters(&mut self) { let config = oakcommon::configstore::ConfigStore::instance(); let width = config.get_int(None, "DefaultSequenceWidth", 1920); let height = config.get_int(None, "DefaultSequenceHeight", 1080); let sample_rate = config.get_int(None, "DefaultSequenceAudioFrequency", 48000); let fps_num = config.get_int(None, "DefaultSequenceFrameRateNum", 30); let fps_den = config.get_int(None, "DefaultSequenceFrameRateDen", 1); self.video_params = vec![VideoParams { width, height, frame_rate: oakcore_rs::Rational::new(fps_num as i64, fps_den as i64), pixel_format: 4, // f32 channels: 4, }]; self.audio_params = vec![AudioParams { sample_rate, channel_layout: 0x3, // stereo format: 4, // f32 }]; } /// Recompute the cached lengths from the track lists (C++ /// `ViewerOutput::verify_length()`). pub fn verify_length( &mut self, lengths: ( oakcore_rs::Rational, oakcore_rs::Rational, oakcore_rs::Rational, ), ) { let (video, audio, overall) = lengths; self.last_length = overall; let _ = (video, audio); } /// Total stream counts. pub fn video_stream_count(&self) -> usize { self.video_params.len() } /// Audio stream count. pub fn audio_stream_count(&self) -> usize { self.audio_params.len() } } impl NodeBehavior for SequenceBehavior { fn name(&self) -> &str { "Sequence" } fn type_id(&self) -> &str { "org.olivevideoeditor.Olive.sequence" } fn categories(&self) -> &[Category] { &[Category::Output] } fn duplicate(&self, _core: &NodeCore) -> Option> { Some(Box::new(SequenceBehavior::new())) } /// Custom project save (C++ `Sequence::SaveCustom` writes the /// workarea and markers; those live behind opaque oaktimeline /// handles in Rust, so the track-list references are all that /// persists). fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) { let _ = core; if !self.track_lists.is_empty() { writer.start_element("tracklists"); for t in &self.track_lists { writer.text_element("tracklist", &t.identity().to_string()); } writer.end_element(); // tracklists } } /// Custom project load (C++ `Sequence::LoadCustom`): the track-list /// references are collected here and resolved to live ids by the /// serializer's post-load pass; the C++ workarea/markers segments /// are skipped (opaque handles). fn load_custom( &mut self, _core: &mut NodeCore, reader: &mut dyn crate::serializer::XmlRead, ) -> bool { while reader.next_start_element() { match reader.name() { "tracklists" => { while reader.next_start_element() { if reader.name() == "tracklist" { if let Some(id) = crate::serializer::parse_node_ref(&reader.read_element_text()) { self.track_lists.push(id); } } else { reader.skip_current_element(); } } } _ => reader.skip_current_element(), } } true } fn as_any(&self) -> Option<&dyn std::any::Any> { Some(self) } fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { Some(self) } } impl Default for SequenceBehavior { fn default() -> Self { SequenceBehavior::new() } }