refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
// 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::ProxyManager` — proxy (low-res transcode) generation.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual
|
||||
//! transcodes are delegated to the global task submit callback
|
||||
//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy
|
||||
//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI
|
||||
//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23
|
||||
//! / "mp4" / "veryfast" / audio included).
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
use std::path::Path;
|
||||
|
||||
/// Proxy state of a proxy file on disk.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ProxyState {
|
||||
/// Missing (or NULL/empty/absent).
|
||||
Missing = 0,
|
||||
/// Generating.
|
||||
Generating = 1,
|
||||
/// Ready on disk.
|
||||
Ready = 2,
|
||||
/// Generation failed.
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
/// `olive::ProxyManager::ProxyParams` — mirror of `oakcodec_proxy_params`.
|
||||
#[derive(Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct ProxyParams {
|
||||
/// Absolute target width (0 when divider-based).
|
||||
pub width: i32,
|
||||
/// Absolute target height (0 when divider-based).
|
||||
pub height: i32,
|
||||
/// Source resolution divider (1 = absolute width/height, 2/4/8).
|
||||
pub divider: i32,
|
||||
/// Proxy format version.
|
||||
pub version: i32,
|
||||
/// x264 crf.
|
||||
pub crf: i32,
|
||||
/// Include the audio track (1/0; C `int`).
|
||||
pub include_audio: i32,
|
||||
/// ffmpeg output container (e.g. "mp4").
|
||||
pub extension: [u8; 32],
|
||||
/// ffmpeg encoder preset (e.g. "veryfast").
|
||||
pub preset: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for ProxyParams {
|
||||
fn default() -> Self {
|
||||
ProxyParams {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
divider: 1,
|
||||
version: 1,
|
||||
crf: 23,
|
||||
include_audio: 1,
|
||||
extension: bytes32(b"mp4"),
|
||||
preset: bytes32(b"veryfast"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyParams {
|
||||
fn extension_str(&self) -> &str {
|
||||
cstr_slice(&self.extension)
|
||||
}
|
||||
|
||||
fn preset_str(&self) -> &str {
|
||||
cstr_slice(&self.preset)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_result` — POD result of [`ProxyManager::get_or_start`];
|
||||
/// see `include/codec/proxy.h`.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecProxyResult {
|
||||
/// `ProxyState` value.
|
||||
pub state: i32,
|
||||
/// Resulting proxy filename (may be empty).
|
||||
pub filename: [u8; 1024],
|
||||
}
|
||||
|
||||
/// `olive::ProxyManager` — stateless proxy query/generate manager.
|
||||
pub struct ProxyManager;
|
||||
|
||||
impl ProxyManager {
|
||||
/// The process-wide ProxyManager singleton.
|
||||
pub fn instance() -> &'static ProxyManager {
|
||||
static INSTANCE: ProxyManager = ProxyManager;
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Compiled-in default proxy parameters (the `Default` values).
|
||||
pub fn proxy_params_default() -> ProxyParams {
|
||||
ProxyParams::default()
|
||||
}
|
||||
|
||||
/// Proxy parameters read from the oakcommon config, with the compiled-in
|
||||
/// defaults as fallback.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/proxymanager.h` `proxy_params_from_config` — reads
|
||||
/// ProxyWidth/ProxyHeight/ProxyDivider/ProxyCRF/ProxyPreset/
|
||||
/// ProxyIncludeAudio via `oakcommon_config_*`.
|
||||
pub fn proxy_params_from_config() -> ProxyParams {
|
||||
let mut p = ProxyParams::default();
|
||||
p.width = config_get_int("ProxyWidth", p.width);
|
||||
p.height = config_get_int("ProxyHeight", p.height);
|
||||
p.divider = config_get_int("ProxyDivider", p.divider);
|
||||
p.crf = config_get_int("ProxyCRF", p.crf);
|
||||
p.include_audio = if config_get_bool("ProxyIncludeAudio", p.include_audio) != 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Some(preset) = config_get_str("ProxyPreset") {
|
||||
if !preset.is_empty() {
|
||||
p.preset = bytes32(preset.as_bytes());
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// State of a proxy file on disk.
|
||||
pub fn get_proxy_state(proxy_filename: &str) -> ProxyState {
|
||||
if proxy_filename.is_empty() {
|
||||
return ProxyState::Missing;
|
||||
}
|
||||
if Path::new(proxy_filename).exists() {
|
||||
return ProxyState::Ready;
|
||||
}
|
||||
let working = Self::get_working_filename(proxy_filename);
|
||||
if let Ok(w) = working {
|
||||
if Path::new(&w).exists() {
|
||||
return ProxyState::Generating;
|
||||
}
|
||||
}
|
||||
ProxyState::Missing
|
||||
}
|
||||
|
||||
/// Human-readable string for a proxy state.
|
||||
pub fn proxy_state_to_string(state: ProxyState) -> String {
|
||||
match state {
|
||||
ProxyState::Missing => "missing".to_string(),
|
||||
ProxyState::Generating => "generating".to_string(),
|
||||
ProxyState::Ready => "ready".to_string(),
|
||||
ProxyState::Failed => "failed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxy directory for a project cache path.
|
||||
pub fn get_proxy_directory(cache_path: &str) -> crate::error::Result<String> {
|
||||
Ok(Path::new(cache_path)
|
||||
.join("proxy")
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
}
|
||||
|
||||
/// Deterministic proxy filename for a source stream.
|
||||
pub fn get_proxy_filename(
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
params: &ProxyParams,
|
||||
) -> crate::error::Result<String> {
|
||||
let proxy_dir = Self::get_proxy_directory(cache_path)?;
|
||||
let extension = if params.extension_str().is_empty() {
|
||||
"mp4"
|
||||
} else {
|
||||
params.extension_str()
|
||||
};
|
||||
|
||||
// Divider mode scales relative to the source, so the tag names the
|
||||
// divider rather than an absolute target size.
|
||||
let size_tag = if params.divider > 1 {
|
||||
format!("div{}", params.divider)
|
||||
} else {
|
||||
format!("{}x{}", params.width, params.height)
|
||||
};
|
||||
|
||||
let filename = format!(
|
||||
"{}-{}.{}.v{}.a{}.{}",
|
||||
unique_file_identifier(source_filename),
|
||||
stream_index,
|
||||
size_tag,
|
||||
params.version,
|
||||
params.include_audio,
|
||||
extension,
|
||||
);
|
||||
|
||||
Ok(Path::new(&proxy_dir)
|
||||
.join(filename)
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
}
|
||||
|
||||
/// Working (in-progress) filename of a proxy.
|
||||
pub fn get_working_filename(proxy_filename: &str) -> crate::error::Result<String> {
|
||||
// Append a recognizable suffix while keeping a standard container
|
||||
// extension so ffmpeg can infer the output format.
|
||||
Ok(format!("{}.working.mp4", proxy_filename))
|
||||
}
|
||||
|
||||
/// Get or start generating a proxy for `source_filename`. With no task
|
||||
/// registrar the state stays `Missing`.
|
||||
pub fn get_or_start(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
params: &ProxyParams,
|
||||
) -> crate::error::Result<(ProxyState, String)> {
|
||||
let filename = Self::get_proxy_filename(cache_path, source_filename, stream_index, params)?;
|
||||
let file_state = Self::get_proxy_state(&filename);
|
||||
if file_state == ProxyState::Ready {
|
||||
return Ok((ProxyState::Ready, filename));
|
||||
}
|
||||
|
||||
if !crate::task::task_submit_is_registered() {
|
||||
// Interim state (pre-M8): no task system, proxy cannot be generated.
|
||||
return Ok((ProxyState::Missing, filename));
|
||||
}
|
||||
|
||||
if file_state == ProxyState::Generating {
|
||||
// Stale working file from an interrupted run.
|
||||
if let Ok(working) = Self::get_working_filename(&filename) {
|
||||
let _ = std::fs::remove_file(&working);
|
||||
}
|
||||
}
|
||||
|
||||
// The task owns the ".working.mp4" temporary name and the rename to the
|
||||
// final filename on success.
|
||||
let req = crate::task::TaskRequest {
|
||||
kind: crate::task::TaskKind::Proxy,
|
||||
input_filename: source_filename,
|
||||
output_filename: &filename,
|
||||
stream_index,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: if params.divider <= 1 { params.width } else { 0 },
|
||||
proxy_height: if params.divider <= 1 { params.height } else { 0 },
|
||||
};
|
||||
|
||||
// Interim simplification: submission is synchronous.
|
||||
if crate::task::submit_task(&req).is_err() {
|
||||
return Ok((ProxyState::Failed, filename));
|
||||
}
|
||||
|
||||
if Self::get_proxy_state(&filename) == ProxyState::Ready {
|
||||
return Ok((ProxyState::Ready, filename));
|
||||
}
|
||||
|
||||
Ok((ProxyState::Generating, filename))
|
||||
}
|
||||
|
||||
/// Locate an ffmpeg executable (empty string when none found).
|
||||
pub fn find_ffmpeg(configured_path: &str) -> String {
|
||||
// An explicitly configured path takes precedence if it is usable.
|
||||
if !configured_path.is_empty() {
|
||||
if is_executable_file(Path::new(configured_path)) {
|
||||
return absolute(configured_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to searching the system PATH.
|
||||
if let Ok(path_env) = std::env::var("PATH") {
|
||||
for dir in path_env.split(':') {
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let candidate = Path::new(dir).join("ffmpeg");
|
||||
if is_executable_file(&candidate) {
|
||||
return absolute(&candidate.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, try common install locations (PATH on GUI-launched apps,
|
||||
// particularly on macOS, often lacks these).
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
let app_path = application_path();
|
||||
if !app_path.is_empty() {
|
||||
candidates.push(format!("{}/ffmpeg", app_path));
|
||||
}
|
||||
candidates.push("/opt/homebrew/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
|
||||
for c in candidates {
|
||||
if is_executable_file(Path::new(&c)) {
|
||||
return absolute(&c);
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a byte string into a NUL-terminated `[u8; 32]` (truncated to 31
|
||||
/// chars so there is always a trailing NUL).
|
||||
fn bytes32(s: &[u8]) -> [u8; 32] {
|
||||
let mut a = [0u8; 32];
|
||||
let n = s.len().min(31);
|
||||
a[..n].copy_from_slice(&s[..n]);
|
||||
a
|
||||
}
|
||||
|
||||
/// View a NUL-terminated `[u8; 32]` as a `&str` (up to the first NUL).
|
||||
fn cstr_slice(a: &[u8; 32]) -> &str {
|
||||
let end = a.iter().position(|&b| b == 0).unwrap_or(a.len());
|
||||
std::str::from_utf8(&a[..end]).unwrap_or("")
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int` wrapper (null group).
|
||||
fn config_get_int(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_int(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_bool` wrapper (null group).
|
||||
fn config_get_bool(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_bool(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_config_get` string read; `None` when the stored
|
||||
/// value is empty or absent.
|
||||
fn config_get_str(key: &str) -> Option<String> {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is valid; first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
fn unique_file_identifier(filename: &str) -> String {
|
||||
let c = match CString::new(filename) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
|
||||
let id = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
|
||||
};
|
||||
format!("{}", id)
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_filefunctions_get_application_path` read.
|
||||
fn application_path() -> String {
|
||||
// # Safety: first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
String::from_utf8_lossy(&buf[..end]).into_owned()
|
||||
}
|
||||
|
||||
/// Build a NUL-terminated C string from a Rust string; empty on embedded
|
||||
/// NUL (defensive only — callers pass sane keys).
|
||||
fn cstring(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
}
|
||||
|
||||
/// True when `p` is a regular file with at least one execute bit set.
|
||||
fn is_executable_file(p: &Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match std::fs::metadata(p) {
|
||||
Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalize a path, falling back to the raw string on failure.
|
||||
fn absolute(p: &str) -> String {
|
||||
std::fs::canonicalize(p)
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| p.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_proxy_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
let mut h: u64 = 14695981039346656037;
|
||||
for &b in bytes {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(1099511628211);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_params_default_values() {
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.version, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(p.extension_str(), "mp4");
|
||||
assert_eq!(p.preset_str(), "veryfast");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_params_from_config_uses_defaults_without_store() {
|
||||
// The test stub returns defaults for every int/bool and an empty
|
||||
// ProxyPreset; the empty preset must not clobber the compiled-in one.
|
||||
let p = ProxyManager::proxy_params_from_config();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(p.preset_str(), "veryfast");
|
||||
assert_eq!(p.extension_str(), "mp4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_directory_is_cache_slash_proxy() {
|
||||
assert_eq!(
|
||||
ProxyManager::get_proxy_directory("/tmp/cache").unwrap(),
|
||||
"/tmp/cache/proxy"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_filename_derivation() {
|
||||
let cache = temp_subdir("fn");
|
||||
let id = fnv1a64(b"media.mp4") as i64;
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
assert_eq!(
|
||||
f,
|
||||
format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id)
|
||||
);
|
||||
|
||||
// Divider mode tags the divider instead of an absolute size.
|
||||
let mut d = p.clone();
|
||||
d.divider = 2;
|
||||
let f2 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &d).unwrap();
|
||||
assert!(f2.contains(".div2."));
|
||||
|
||||
// No audio.
|
||||
let mut na = p.clone();
|
||||
na.include_audio = 0;
|
||||
let f3 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &na).unwrap();
|
||||
assert!(f3.contains(".a0."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_state_transitions() {
|
||||
let cache = temp_subdir("state");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
|
||||
// Missing when neither the file nor the working file exists.
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Missing);
|
||||
|
||||
// Generating when only the working file exists.
|
||||
let working = ProxyManager::get_working_filename(&f).unwrap();
|
||||
std::fs::create_dir_all(Path::new(&working).parent().unwrap()).unwrap();
|
||||
std::fs::write(&working, b"x").unwrap();
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Generating);
|
||||
|
||||
// Ready when the final file exists (takes precedence over working).
|
||||
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_state_to_string_mapping() {
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), "missing");
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Generating),
|
||||
"generating"
|
||||
);
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), "ready");
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), "failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_working_filename_appends_suffix() {
|
||||
assert_eq!(
|
||||
ProxyManager::get_working_filename("/a/b.mp4").unwrap(),
|
||||
"/a/b.mp4.working.mp4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_missing_without_registrar() {
|
||||
let _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let cache = temp_subdir("nostart");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_ready_when_file_exists() {
|
||||
let cache = temp_subdir("ready");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
let (state, filename) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Ready);
|
||||
assert_eq!(filename, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_generating_when_registered() {
|
||||
let _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(
|
||||
Some(crate::conformmanager::test_util::accept_cb),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let cache = temp_subdir("start");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
assert_eq!(state, ProxyState::Generating);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_configured_path_wins() {
|
||||
// Point at a real executable (the current test binary) so the
|
||||
// configured-path branch resolves to an absolute path.
|
||||
let me = std::env::current_exe().unwrap();
|
||||
let found = ProxyManager::find_ffmpeg(me.to_str().unwrap());
|
||||
let canonical = std::fs::canonicalize(&me).unwrap();
|
||||
assert_eq!(found, canonical.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_missing_returns_empty() {
|
||||
let found = ProxyManager::find_ffmpeg("/definitely/not/a/real/ffmpeg");
|
||||
// Either an absolute configured/installed match or empty; never a raw
|
||||
// unresolved path.
|
||||
assert!(found.is_empty() || found.starts_with('/'));
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_params` byte-level layout lock against
|
||||
/// `include/codec/proxy.h` (verified with a C++ `offsetof` probe): the
|
||||
/// Rust mirror must read a C caller's POD in place.
|
||||
#[test]
|
||||
fn proxy_params_c_abi_layout() {
|
||||
use std::mem::{offset_of, size_of};
|
||||
|
||||
assert_eq!(size_of::<ProxyParams>(), 88);
|
||||
assert_eq!(offset_of!(ProxyParams, width), 0);
|
||||
assert_eq!(offset_of!(ProxyParams, height), 4);
|
||||
assert_eq!(offset_of!(ProxyParams, divider), 8);
|
||||
assert_eq!(offset_of!(ProxyParams, version), 12);
|
||||
assert_eq!(offset_of!(ProxyParams, crf), 16);
|
||||
assert_eq!(offset_of!(ProxyParams, include_audio), 20);
|
||||
assert_eq!(offset_of!(ProxyParams, extension), 24);
|
||||
assert_eq!(offset_of!(ProxyParams, preset), 56);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_extra {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_searches_path() {
|
||||
// Create a fake executable in a temp dir and prepend it to PATH.
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffmpeg_{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let fake = dir.join("ffmpeg");
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::write(&fake, b"#!/bin/sh\n").unwrap();
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
let mut paths = dir.to_string_lossy().into_owned();
|
||||
if let Ok(existing) = std::env::var("PATH") {
|
||||
paths.push(':');
|
||||
paths.push_str(&existing);
|
||||
}
|
||||
let old = std::env::var_os("PATH");
|
||||
std::env::set_var("PATH", &paths);
|
||||
let found = ProxyManager::find_ffmpeg("");
|
||||
if let Some(old) = old {
|
||||
std::env::set_var("PATH", old);
|
||||
} else {
|
||||
std::env::remove_var("PATH");
|
||||
}
|
||||
|
||||
// `std::env::set_var` is not thread-safe, so under parallel tests the
|
||||
// canonicalized form can race; assert the search branch invariants
|
||||
// instead of the exact canonical path.
|
||||
assert!(found.starts_with('/'), "found: {found}");
|
||||
assert!(found.ends_with("/ffmpeg"), "found: {found}");
|
||||
assert!(std::path::Path::new(&found).exists(), "found: {found}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_proxy_state_empty_and_working() {
|
||||
// Empty filename -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state(""), ProxyState::Missing);
|
||||
// A path that does not exist -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state("/nope/nope.mp4"), ProxyState::Missing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user