Files
oak-editor/crates/oak-codec/src/proxymanager.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
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.
2026-08-22 16:58:37 +08:00

770 lines
23 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::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 store
//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23
//! / "mp4" / "veryfast" / audio included).
use oak_common::configstore::ConfigStore;
use oak_common::filefunctions::FileFunctions;
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,
}
impl TryFrom<i32> for ProxyState {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(ProxyState::Missing),
1 => Ok(ProxyState::Generating),
2 => Ok(ProxyState::Ready),
3 => Ok(ProxyState::Failed),
_ => Err(()),
}
}
}
/// `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(),
}
}
/// The proxy state a string name stands for (`Missing` for anything
/// unrecognized; also accepts the numeric form the Rust serializer used
/// before the string form landed).
pub fn proxy_state_from_string(name: &str) -> ProxyState {
match name.trim() {
"generating" | "1" => ProxyState::Generating,
"ready" | "2" => ProxyState::Ready,
"failed" | "3" => ProxyState::Failed,
_ => ProxyState::Missing,
}
}
/// Whether a proxy filename was generated with an audio track (the
/// generation tags audio-including proxies `.a1.`).
pub fn proxy_filename_has_audio(proxy_filename: &str) -> bool {
std::path::Path::new(proxy_filename)
.file_name()
.map(|n| n.to_string_lossy().contains(".a1."))
.unwrap_or(false)
}
/// 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 (split per platform:
// `;` on Windows, `:` elsewhere — see [`split_path_env`]).
if let Ok(path_env) = std::env::var("PATH") {
for dir in split_path_env(&path_env) {
let candidate = Path::new(&dir).join(ffmpeg_exe_name());
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!("{}/{}", app_path, ffmpeg_exe_name()));
}
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());
// Windows-specific install locations (GUI-launched apps can also
// start with a minimal PATH there).
#[cfg(target_os = "windows")]
{
// The official Windows installer defaults to
// %LOCALAPPDATA%\Programs\ffmpeg\bin.
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
candidates.push(format!(
"{}/Programs/ffmpeg/bin/{}",
local_app_data,
ffmpeg_exe_name()
));
}
// Oak's user configuration directory.
if let Ok(config_dir) = FileFunctions::new().get_configuration_location() {
candidates.push(format!("{}/{}", config_dir, ffmpeg_exe_name()));
}
}
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 {
ConfigStore::instance().get_int(None, key, default)
}
/// `oakcommon_config_get_bool` wrapper (null group).
fn config_get_bool(key: &str, default: i32) -> i32 {
ConfigStore::instance().get_bool(None, key, default)
}
/// `oakcommon_config_get` string read; `None` when the stored value is
/// empty or absent.
fn config_get_str(key: &str) -> Option<String> {
match ConfigStore::instance().get(None, key) {
Ok(s) if !s.is_empty() => Some(s),
_ => None,
}
}
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
fn unique_file_identifier(filename: &str) -> String {
FileFunctions::new()
.get_unique_file_identifier(filename)
.unwrap_or_default()
}
/// `oakcommon_filefunctions_get_application_path` read.
fn application_path() -> String {
FileFunctions::new()
.get_application_path()
.unwrap_or_default()
}
/// Split a PATH-style variable into its non-empty directory entries. The
/// separator is platform-specific (`;` on Windows, `:` elsewhere);
/// `std::env::split_paths` already applies that rule natively.
fn split_path_env(raw: &str) -> Vec<String> {
std::env::split_paths(raw)
.map(|p| p.to_string_lossy().into_owned())
.filter(|p| !p.is_empty())
.collect()
}
/// The executable file name to look for: `ffmpeg.exe` on Windows,
/// `ffmpeg` everywhere else.
fn ffmpeg_exe_name() -> &'static str {
if cfg!(windows) {
"ffmpeg.exe"
} else {
"ffmpeg"
}
}
/// True when `p` is a regular file that can be run as a program. Unix
/// checks for at least one execute bit; Windows has no permission bits, so
/// any regular file counts.
fn is_executable_file(p: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(p) {
Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0,
_ => false,
}
}
#[cfg(windows)]
{
matches!(std::fs::metadata(p), Ok(md) if md.is_file())
}
}
/// 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()
}
#[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(),
// Path::join separators are platform-native (\ on Windows).
std::path::Path::new("/tmp/cache")
.join("proxy")
.to_string_lossy()
);
}
#[test]
fn proxy_filename_derivation() {
let cache = temp_subdir("fn");
let p = ProxyManager::proxy_params_default();
// A missing source carries no unique-file identifier
// (get_unique_file_identifier is empty for non-existent files —
// C++ parity), so the name is the plain size/version/audio tags.
let missing = std::path::Path::new(&temp_subdir("missing")).join("nope.mp4");
let f = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &p).unwrap();
let plain = std::path::Path::new(&cache)
.join("proxy")
.join("-0.1280x720.v1.a1.mp4")
.to_string_lossy()
.into_owned();
assert_eq!(f, plain);
// An existing source embeds a stable per-file identifier.
let existing = std::path::Path::new(&temp_subdir("existing")).join("real.mp4");
std::fs::write(&existing, b"media").unwrap();
let f1 =
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
let f2 =
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
assert!(
f1.contains("-0.1280x720.v1.a1.mp4"),
"size/version/audio tags present: {f1}"
);
assert!(f1 != plain, "id embedded: {f1}");
assert_eq!(f1, f2, "the identifier is stable for the same file");
// Divider mode tags the divider instead of an absolute size.
let mut d = p.clone();
d.divider = 2;
let f3 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &d).unwrap();
assert!(f3.contains(".div2."));
// No audio.
let mut na = p.clone();
na.include_audio = 0;
let f4 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &na).unwrap();
assert!(f4.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() || std::path::Path::new(&found).is_absolute(),
"found: {found}"
);
}
/// `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::*;
/// Unix-only: builds a fake `ffmpeg` shell script with a mode-0755
/// chmod; Windows has no permission bits and searches for `ffmpeg.exe`.
#[cfg(unix)]
#[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 split_path_env_uses_platform_separator() {
// The separator is platform-dependent, so the expected input is
// gated: `:` on Unix, `;` on Windows.
#[cfg(unix)]
{
assert_eq!(split_path_env("a:b:c"), ["a", "b", "c"]);
// Empty entries are skipped.
assert_eq!(split_path_env("a::b:"), ["a", "b"]);
assert_eq!(split_path_env("solo"), ["solo"]);
}
#[cfg(windows)]
{
assert_eq!(split_path_env("a;b;c"), ["a", "b", "c"]);
assert_eq!(split_path_env("a;;b;"), ["a", "b"]);
assert_eq!(split_path_env("solo"), ["solo"]);
}
}
#[test]
fn ffmpeg_exe_name_is_platform_specific() {
#[cfg(unix)]
assert_eq!(ffmpeg_exe_name(), "ffmpeg");
#[cfg(windows)]
assert_eq!(ffmpeg_exe_name(), "ffmpeg.exe");
}
#[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
);
}
}