CI / Build & test (Windows) (push) Failing after 7s
All crates take the oak-* kebab-case naming (oak-audio, oak-codec, oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin, oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: -> oak_core::, ...) across all 226 referencing files. The GUI application moves from the workspace root into crates/oak-app/: src/, build.rs (paths fixed for the new location) and tests/ travel with it, the root Cargo.toml becomes workspace-only ([workspace] + workspace.package + profiles), and the app package inherits the workspace version. The screenshots example becomes a standalone crate examples/simple_player/ with its own Cargo.toml. Every crate now inherits the single workspace version (version.workspace = true), and the workflows' crate paths and the build docs follow the renames. Validated with a clean cargo check --workspace.
338 lines
10 KiB
Rust
338 lines
10 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/>.
|
|
|
|
//! `olive::ConformManager` — pcm waveform cache files for fast scrubbing.
|
|
//!
|
|
//! Mirrors `src/codec/src/conformmanager.h`. Stateless (NOTES.md): actual
|
|
//! conform work is delegated to the global task submit callback
|
|
//! ([`crate::task`]); with no registrar the state queries report
|
|
//! `Unavailable`. Deterministic per-channel filenames derive from the
|
|
//! source + target audio params.
|
|
|
|
use oak_common::filefunctions::FileFunctions;
|
|
use std::path::Path;
|
|
|
|
/// Conform state of one audio stream.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
#[repr(i32)]
|
|
pub enum ConformState {
|
|
/// Conform files exist.
|
|
Exists = 0,
|
|
/// Conform is being generated.
|
|
Generating = 1,
|
|
/// No task registrar; conform unavailable.
|
|
Unavailable = 2,
|
|
}
|
|
|
|
/// `olive::ConformManager` — stateless conform query/produce manager.
|
|
pub struct ConformManager;
|
|
|
|
impl ConformManager {
|
|
/// The process-wide ConformManager singleton.
|
|
pub fn instance() -> &'static ConformManager {
|
|
static INSTANCE: ConformManager = ConformManager;
|
|
&INSTANCE
|
|
}
|
|
|
|
/// Query (and when possible start) the conform of one audio stream.
|
|
///
|
|
/// `wait != 0` treats a post-submit miss as `Unavailable`; `wait == 0`
|
|
/// reports it as `Generating`. Without a task registrar the result is
|
|
/// always `Unavailable`.
|
|
pub fn get_conform_state(
|
|
&self,
|
|
cache_path: &str,
|
|
source_filename: &str,
|
|
stream_index: i32,
|
|
sample_rate: i32,
|
|
channel_layout: u64,
|
|
sample_format: i32,
|
|
wait: bool,
|
|
) -> crate::error::Result<ConformState> {
|
|
let filenames = conform_filenames(
|
|
cache_path,
|
|
source_filename,
|
|
stream_index,
|
|
sample_rate,
|
|
sample_format,
|
|
channel_layout,
|
|
);
|
|
|
|
// Return existing conform if it exists.
|
|
if all_conforms_exist(&filenames) {
|
|
return Ok(ConformState::Exists);
|
|
}
|
|
|
|
// Interim state (pre-M8): no task system, conform cannot be generated.
|
|
if !crate::task::task_submit_is_registered() {
|
|
return Ok(ConformState::Unavailable);
|
|
}
|
|
|
|
// The task owns the ".working" temporary names and the rename to the
|
|
// final per-channel filenames on success; output_filename carries the
|
|
// first channel's final path and the task derives the siblings.
|
|
let req = crate::task::TaskRequest {
|
|
kind: crate::task::TaskKind::Conform,
|
|
input_filename: source_filename,
|
|
output_filename: filenames.first().map(String::as_str).unwrap_or(""),
|
|
stream_index,
|
|
sample_rate,
|
|
channel_layout,
|
|
sample_format,
|
|
proxy_width: 0,
|
|
proxy_height: 0,
|
|
};
|
|
|
|
// Interim simplification: submission is synchronous — we always wait
|
|
// for the submit to return, regardless of `wait`.
|
|
if crate::task::submit_task(&req).is_err() {
|
|
return Ok(ConformState::Unavailable);
|
|
}
|
|
|
|
if all_conforms_exist(&filenames) {
|
|
return Ok(ConformState::Exists);
|
|
}
|
|
|
|
if wait {
|
|
// Synchronous wait already happened and the conform still does not
|
|
// exist: report the wait as failed.
|
|
return Ok(ConformState::Unavailable);
|
|
}
|
|
|
|
Ok(ConformState::Generating)
|
|
}
|
|
|
|
/// Number of conform (pcm) files for the given stream/params — one per
|
|
/// channel; 0 on invalid arguments.
|
|
pub fn get_conform_filename_count(
|
|
&self,
|
|
_cache_path: &str,
|
|
_source_filename: &str,
|
|
_stream_index: i32,
|
|
_sample_rate: i32,
|
|
channel_layout: u64,
|
|
_sample_format: i32,
|
|
) -> usize {
|
|
channel_layout.count_ones() as usize
|
|
}
|
|
|
|
/// The `index`-th conform filename.
|
|
pub fn get_conform_filename(
|
|
&self,
|
|
cache_path: &str,
|
|
source_filename: &str,
|
|
stream_index: i32,
|
|
sample_rate: i32,
|
|
channel_layout: u64,
|
|
sample_format: i32,
|
|
index: usize,
|
|
) -> crate::error::Result<String> {
|
|
let filenames = conform_filenames(
|
|
cache_path,
|
|
source_filename,
|
|
stream_index,
|
|
sample_rate,
|
|
sample_format,
|
|
channel_layout,
|
|
);
|
|
filenames
|
|
.get(index)
|
|
.cloned()
|
|
.ok_or(crate::error::Error::NotFound)
|
|
}
|
|
}
|
|
|
|
/// Deterministic conform base name plus per-channel pcm filenames, mirroring
|
|
/// `ConformManager::get_conformed_filename`: one file per channel under
|
|
/// `cache_path`, named `<identifier>-<stream>.<rate>.<format>.<layout>.<i>.pcm`.
|
|
fn conform_filenames(
|
|
cache_path: &str,
|
|
source_filename: &str,
|
|
stream_index: i32,
|
|
sample_rate: i32,
|
|
sample_format: i32,
|
|
channel_layout: u64,
|
|
) -> Vec<String> {
|
|
let count = channel_layout.count_ones() as usize;
|
|
let base = format!(
|
|
"{}-{}.{}.{}.{}",
|
|
unique_file_identifier(source_filename),
|
|
stream_index,
|
|
sample_rate,
|
|
sample_format,
|
|
channel_layout,
|
|
);
|
|
|
|
let mut out = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let p = Path::new(cache_path).join(format!("{}.{}.pcm", base, i));
|
|
out.push(p.to_string_lossy().into_owned());
|
|
}
|
|
out
|
|
}
|
|
|
|
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
|
|
fn unique_file_identifier(filename: &str) -> String {
|
|
FileFunctions::new()
|
|
.get_unique_file_identifier(filename)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// True when every conform filename already exists on disk.
|
|
fn all_conforms_exist(filenames: &[String]) -> bool {
|
|
filenames.iter().all(|f| Path::new(f).exists())
|
|
}
|
|
|
|
/// Shared test support: serializes access to the global task-submit registry
|
|
/// (unit tests run in parallel and would otherwise clear each other's
|
|
/// registration) and provides a callback that accepts any task.
|
|
#[cfg(test)]
|
|
pub(crate) mod test_util {
|
|
use crate::error::OAKCODEC_OK;
|
|
use crate::task::OakCodecTaskRequest;
|
|
|
|
/// Serializes every test that mutates the task-submit registry.
|
|
pub static REG_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
/// A task-submit callback that accepts every request (no-op).
|
|
pub unsafe extern "C" fn accept_cb(
|
|
_req: *const OakCodecTaskRequest,
|
|
_ud: *mut std::ffi::c_void,
|
|
) -> i32 {
|
|
OAKCODEC_OK
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn temp_subdir(name: &str) -> String {
|
|
let dir =
|
|
std::env::temp_dir().join(format!("oakcodec_conform_{}_{}", name, std::process::id()));
|
|
let _ = std::fs::create_dir_all(&dir);
|
|
dir.to_string_lossy().into_owned()
|
|
}
|
|
|
|
#[test]
|
|
fn unique_identifier_uses_file_metadata() {
|
|
// The identifier is the 16-digit FNV-1a hex of (absolute path +
|
|
// mtime); a missing file has no identifier.
|
|
assert_eq!(unique_file_identifier("no-such-file.mp4"), "");
|
|
let dir = temp_subdir("id");
|
|
let file = std::path::Path::new(&dir).join("media.mp4");
|
|
std::fs::write(&file, b"x").unwrap();
|
|
let id = unique_file_identifier(&file.to_string_lossy());
|
|
assert_eq!(id.len(), 16);
|
|
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
|
|
// Deterministic: same input, same id.
|
|
assert_eq!(id, unique_file_identifier(&file.to_string_lossy()));
|
|
let other = std::path::Path::new(&dir).join("other.mp4");
|
|
std::fs::write(&other, b"y").unwrap();
|
|
// Different input, different id.
|
|
assert_ne!(id, unique_file_identifier(&other.to_string_lossy()));
|
|
}
|
|
|
|
#[test]
|
|
fn filename_count_from_channel_layout() {
|
|
let m = ConformManager::instance();
|
|
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x3, 0), 2); // stereo
|
|
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x4, 0), 1); // mono
|
|
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0, 0), 0); // invalid
|
|
assert_eq!(
|
|
m.get_conform_filename_count("c", "s", 0, 48000, 0x60F, 0),
|
|
6
|
|
); // 5.1
|
|
}
|
|
|
|
#[test]
|
|
fn conform_filename_derivation_and_range() {
|
|
let m = ConformManager::instance();
|
|
let src_dir = temp_subdir("names");
|
|
let src = std::path::Path::new(&src_dir).join("media.mp4");
|
|
std::fs::write(&src, b"x").unwrap();
|
|
let cache = temp_subdir("names");
|
|
let id = unique_file_identifier(&src.to_string_lossy());
|
|
let base = format!("{}-0.48000.0.3", id);
|
|
let f0 = m
|
|
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 0)
|
|
.unwrap();
|
|
let f1 = m
|
|
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 1)
|
|
.unwrap();
|
|
assert_eq!(
|
|
f0,
|
|
std::path::Path::new(&cache)
|
|
.join(format!("{base}.0.pcm"))
|
|
.to_string_lossy()
|
|
);
|
|
assert_eq!(
|
|
f1,
|
|
std::path::Path::new(&cache)
|
|
.join(format!("{base}.1.pcm"))
|
|
.to_string_lossy()
|
|
);
|
|
// Out of range.
|
|
assert!(matches!(
|
|
m.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 5),
|
|
Err(crate::error::Error::NotFound)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn get_conform_state_unavailable_without_registrar() {
|
|
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
|
// Ensure no registrar is left over.
|
|
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
|
let cache = temp_subdir("unavail");
|
|
let s = ConformManager::instance()
|
|
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
|
.unwrap();
|
|
assert_eq!(s, ConformState::Unavailable);
|
|
}
|
|
|
|
#[test]
|
|
fn get_conform_state_exists_when_files_present() {
|
|
let cache = temp_subdir("exists");
|
|
let m = ConformManager::instance();
|
|
for i in 0..2 {
|
|
let f = m
|
|
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, i)
|
|
.unwrap();
|
|
std::fs::write(&f, b"pcm").unwrap();
|
|
}
|
|
let s = m
|
|
.get_conform_state(&cache, "media.mp4", 0, 48000, 0x3, 0, false)
|
|
.unwrap();
|
|
assert_eq!(s, ConformState::Exists);
|
|
}
|
|
|
|
#[test]
|
|
fn get_conform_state_generating_when_registered() {
|
|
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
|
crate::task::set_task_submit_cb_extern(
|
|
Some(super::test_util::accept_cb),
|
|
std::ptr::null_mut(),
|
|
);
|
|
let cache = temp_subdir("generating");
|
|
let s = ConformManager::instance()
|
|
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
|
.unwrap();
|
|
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
|
assert_eq!(s, ConformState::Generating);
|
|
}
|
|
}
|