Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
308 lines
8.9 KiB
Rust
308 lines
8.9 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/>.
|
|
|
|
//! `OIIODecoder` / `OIIOEncoder` — the OpenImageIO-backed implementations.
|
|
//!
|
|
//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO
|
|
//! frame conversion goes through the local
|
|
//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping
|
|
//! functions.
|
|
//!
|
|
//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every
|
|
//! operation that would touch the media engine is a documented stub returning
|
|
//! [`crate::error::Error::Failed`]; only bookkeeping that keeps the
|
|
//! decoder/encoder safe to use when closed succeeds. The stream is still
|
|
//! recorded on [`Decoder::open`] so [`Decoder::stream`] reflects the target,
|
|
//! and [`Decoder::close`] / [`Encoder::close`] are no-ops.
|
|
|
|
use crate::decoder::{CodecStream, Decoder, RetrieveVideoParams};
|
|
use crate::encoder::Encoder;
|
|
use crate::encodingparams::EncodingParams;
|
|
|
|
/// `olive::OIIODecoder` — OpenImageIO-backed media decoder (still images).
|
|
pub struct OIIODecoder {
|
|
/// Opened stream (locked).
|
|
stream: std::sync::Mutex<Option<CodecStream>>,
|
|
}
|
|
|
|
impl OIIODecoder {
|
|
/// Error returned for operations that need the missing `liboakoiio`.
|
|
const NOT_AVAILABLE: &'static str =
|
|
"OIIO decoding is not available in this build (needs liboakoiio dylib)";
|
|
}
|
|
|
|
impl Decoder for OIIODecoder {
|
|
fn id(&self) -> String {
|
|
"oiio".to_string()
|
|
}
|
|
|
|
fn supports_video(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_audio(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn probe(
|
|
&self,
|
|
_filename: &str,
|
|
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
|
|
) -> Option<crate::footagedescription::FootageDescription> {
|
|
// Probing is a dylib operation; without it we cannot report anything.
|
|
None
|
|
}
|
|
|
|
fn open(&self, stream: &CodecStream) -> crate::error::Result<()> {
|
|
// Record the requested stream so `stream()` reflects the open target,
|
|
// but actual decoding is unavailable without the dylib.
|
|
*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = Some(stream.clone());
|
|
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
|
}
|
|
|
|
fn close(&self) -> crate::error::Result<()> {
|
|
// Trait contract: "safe when closed". Clear the opened stream and
|
|
// no-op; nothing was ever decoded.
|
|
*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
|
Ok(())
|
|
}
|
|
|
|
fn stream(&self) -> CodecStream {
|
|
self.stream
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.clone()
|
|
.unwrap_or_else(CodecStream::new)
|
|
}
|
|
|
|
fn retrieve_video_frame(
|
|
&self,
|
|
_p: &RetrieveVideoParams,
|
|
) -> crate::error::Result<std::sync::Arc<crate::frame::Frame>> {
|
|
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
|
}
|
|
|
|
fn retrieve_video(
|
|
&self,
|
|
_p: &RetrieveVideoParams,
|
|
) -> crate::error::Result<crate::decoder::OakRenderTexture> {
|
|
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
|
}
|
|
|
|
fn retrieve_audio(
|
|
&self,
|
|
_dest: &mut [f32],
|
|
_range: &oakcore_rs::TimeRange,
|
|
_sample_rate: i32,
|
|
_channel_layout: u64,
|
|
) -> crate::error::Result<crate::decoder::RetrieveAudioStatus> {
|
|
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
|
}
|
|
|
|
fn conform_audio(
|
|
&self,
|
|
_output_filenames: &[String],
|
|
_sample_rate: i32,
|
|
_channel_layout: u64,
|
|
_sample_format: i32,
|
|
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
|
|
) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
|
}
|
|
}
|
|
|
|
/// `olive::OIIOEncoder` — OpenImageIO-backed media encoder (still images).
|
|
pub struct OIIOEncoder {
|
|
/// The encoding parameters this encoder was configured with.
|
|
pub params: EncodingParams,
|
|
}
|
|
|
|
impl Encoder for OIIOEncoder {
|
|
fn id(&self) -> String {
|
|
"oiio".to_string()
|
|
}
|
|
|
|
fn supports_video(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_audio(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_subtitles(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_image_sequences(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn is_configurable(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn configure(&self, _params: &EncodingParams) -> crate::error::Result<()> {
|
|
// `configure` writes `self.params`, which an `&self` receiver cannot
|
|
// do, and real encoding needs the dylib anyway.
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn open(&self) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn close(&self) -> crate::error::Result<()> {
|
|
// `Encoder::close` is documented idempotent; with no encoder opened
|
|
// there is nothing to release.
|
|
Ok(())
|
|
}
|
|
|
|
fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn write_subtitle(
|
|
&self,
|
|
_text: &str,
|
|
_in_seconds: f64,
|
|
_out_seconds: f64,
|
|
) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn flush(&self) -> crate::error::Result<()> {
|
|
Err(crate::error::Error::Failed(
|
|
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
|
))
|
|
}
|
|
|
|
fn desired_pixel_format(&self) -> Option<oakcore_rs::PixelFormat> {
|
|
None
|
|
}
|
|
|
|
fn desired_sample_format(&self) -> Option<oakcore_rs::SampleFormat> {
|
|
None
|
|
}
|
|
|
|
fn filename(&self) -> String {
|
|
c_string_1024(&self.params.filename)
|
|
}
|
|
}
|
|
|
|
/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte
|
|
/// buffer, stopping at the first NUL (empty string when unset).
|
|
fn c_string_1024(buf: &[u8; 1024]) -> String {
|
|
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
|
String::from_utf8_lossy(&buf[..end]).into_owned()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::decoder::RetrieveVideoParams;
|
|
use crate::encodingparams::EncodingParams;
|
|
use oakcore_rs::{Rational, TimeRange};
|
|
|
|
fn video_params() -> RetrieveVideoParams {
|
|
RetrieveVideoParams {
|
|
stream: CodecStream::new(),
|
|
time: Rational::new(1, 30),
|
|
length: TimeRange::default(),
|
|
force_range: crate::decoder::K_COLOR_RANGE_DEFAULT,
|
|
is_image_sequence: false,
|
|
image_sequence_digits: 0,
|
|
image_sequence_number: 0,
|
|
mode: crate::decoder::RenderMode::Offline,
|
|
alpha_is_premultiplied: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oiio_decoder_identity_and_stub_operations() {
|
|
let d = OIIODecoder {
|
|
stream: std::sync::Mutex::new(None),
|
|
};
|
|
assert_eq!(d.id(), "oiio");
|
|
// Still images only: no audio support.
|
|
assert!(!d.supports_video());
|
|
assert!(!d.supports_audio());
|
|
assert!(d.probe("any.png", None).is_none());
|
|
|
|
let s = CodecStream::with_block("in.exr".to_string(), 0, None);
|
|
assert!(d.open(&s).is_err());
|
|
assert_eq!(d.stream().filename(), "in.exr");
|
|
assert!(d.close().is_ok());
|
|
assert_eq!(d.stream().filename(), "");
|
|
|
|
assert!(d.retrieve_video_frame(&video_params()).is_err());
|
|
assert!(d.retrieve_video(&video_params()).is_err());
|
|
let mut dest = [0f32; 8];
|
|
assert!(d
|
|
.retrieve_audio(
|
|
&mut dest,
|
|
&TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),
|
|
48000,
|
|
0x3
|
|
)
|
|
.is_err());
|
|
assert!(d
|
|
.conform_audio(&["a.pcm".to_string()], 48000, 0x3, 10, None)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn oiio_encoder_stub_behaviors() {
|
|
let mut params = EncodingParams::default();
|
|
let name = b"out/img.exr";
|
|
params.filename[..name.len()].copy_from_slice(name);
|
|
let e = OIIOEncoder { params };
|
|
|
|
assert_eq!(e.id(), "oiio");
|
|
assert!(!e.supports_video());
|
|
assert!(!e.supports_audio());
|
|
assert!(!e.supports_subtitles());
|
|
assert!(e.supports_image_sequences());
|
|
assert!(e.is_configurable());
|
|
assert_eq!(e.filename(), "out/img.exr");
|
|
assert_eq!(e.desired_pixel_format(), None);
|
|
assert_eq!(e.get_error(), "");
|
|
|
|
assert!(e.configure(&EncodingParams::default()).is_err());
|
|
assert!(e.open().is_err());
|
|
assert!(e.close().is_ok());
|
|
let frame = crate::frame::Frame::new();
|
|
assert!(e.write_video(&frame).is_err());
|
|
assert!(e.write_audio(&[0f32; 4], 1).is_err());
|
|
assert!(e.write_subtitle("hi", 0.0, 1.0).is_err());
|
|
assert!(e.flush().is_err());
|
|
}
|
|
}
|