// 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 .
//! `SaveOTIOTask`, mirroring `src/task/src/project/saveotio/saveotio.h`.
//!
//! Serializes a borrowed project to an OpenTimelineIO (`.otio`) or FCPXML
//! (`.fcpxml`) file through the pure-Rust `oakotio` binding (see `README`
//! decision #6): the project's sequences become `OTIO::Timeline`s
//! (`serialize_timeline` / `serialize_track` / `serialize_track_list`),
//! exactly like the C++ `serialize_*` helpers. The format is dispatched
//! from the filename extension (see [`crate::project::format`]) at the
//! final write only — the serialization itself is shared.
//!
//! **Single-lib note**: the project graph is read through the direct
//! oaknode domain operations in [`crate::nodeops`] (folder children,
//! sequence track lists, track blocks, footage parameters) instead of the
//! deleted oaknode C ABI stubs; the project is an
//! `Arc>` instead of a borrowed `CHandle`.
//!
//! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp
use oakcore_rs::Rational;
use oaknode::id::NodeId;
use oaknode::track::TrackType;
use oakotio::{
Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable,
SerializableCollection, TimeRange, Timeline, Track, Transition,
};
use crate::error::{Error, Result};
use crate::nodeops::{self, ProjectRef};
use crate::project::format::InterchangeFormat;
use crate::task::{Task, TaskBehavior};
/// `Rational(INT_MIN)` — the initial "no track yet" maximum length
/// (`RATIONAL_MIN` in saveotio.cpp).
fn rational_min() -> Rational {
Rational::new(-2147483647, 1)
}
/// An OTIO project-save task. Borrows its project.
pub struct SaveOTIOTask {
/// The shared task base.
pub base: Task,
/// Borrowed project to save (domain project).
pub project: ProjectRef,
/// Output OTIO filename.
pub filename: String,
}
impl SaveOTIOTask {
/// Serialize one sequence into an `OTIO::Timeline`; `None` when the
/// sequence has no usable frame rate or a track fails to serialize.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline)
fn serialize_timeline(project: &ProjectRef, sequence: NodeId) -> Option {
let mut otio_timeline =
Timeline::new(nodeops::node_label(project, sequence));
// Direct video-params value type (single-lib unification); absent
// params leave rate at 0.0 → the documented `None` path.
let (num, den) = nodeops::sequence_video_params(project, sequence, 0)
.map(|vp| vp.frame_rate())
.unwrap_or((0, 1));
let mut rate = 0.0f64;
if den != 0 {
rate = num as f64 / den as f64;
}
if rate.is_nan() || rate <= 0.0 {
return None;
}
let video_list = nodeops::sequence_track_list(project, sequence, TrackType::Video);
let audio_list = nodeops::sequence_track_list(project, sequence, TrackType::Audio);
if let Some(list) = video_list {
if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) {
return None;
}
}
if let Some(list) = audio_list {
if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) {
return None;
}
}
Some(otio_timeline)
}
/// Serialize all tracks of one track list (video or audio) into the
/// timeline. Each track is padded to the list's maximum track length.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)
fn serialize_track_list(
project: &ProjectRef,
list: NodeId,
otio_timeline: &mut Timeline,
sequence_rate: f64,
) -> bool {
let mut max_track_length = rational_min();
let track_count = nodeops::tracklist_track_count(project, list);
for i in 0..track_count {
if let Some(track) = nodeops::tracklist_track_at(project, list, i) {
let length = nodeops::track_length(project, track);
if length > max_track_length {
max_track_length = length;
}
}
}
for i in 0..track_count {
let Some(track) = nodeops::tracklist_track_at(project, list, i) else {
continue;
};
let Some(otio_track) =
Self::serialize_track(project, track, sequence_rate, max_track_length)
else {
return false;
};
otio_timeline
.tracks_mut()
.append_child(Composable::Track(otio_track));
}
true
}
/// Serialize one track: a `Track` whose kind matches the native track
/// type, one OTIO block per native block, plus a trailing `Gap` when the
/// track is shorter than `max_track_length`.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)
fn serialize_track(
project: &ProjectRef,
track: NodeId,
sequence_rate: f64,
max_track_length: Rational,
) -> Option