Files
oak-editor/crates/oak-task/src/proxy.rs
T
Mike-Solar 4babbf5de8
CI / Build & test (Linux) (push) Successful in 24m6s
CI / Build & test (Windows) (push) Successful in 31m14s
core: merge oak-common into oak-core
oak-common is gone; its modules (configstore, xmlutils, ocioutils,
oiioutils, colormath, colortransform, videoparams, ffmpegutils, ...)
now live in oak-core alongside the value types. The render value/GPU
types moved too: backend (wgpu context + DisplayRenderer), color
(ColorProcessor over ocio-rs), texture, frame, and the commonutil
config helpers.

Fix-ups to make the merged tree build and pass tests:

- oak-core Cargo.toml: wgpu back to 25 (the moved backend code is
  written against that API generation); add the toml/quick-xml/image
  deps oak-common carried.
- lib.rs: drop the duplicate 'pub mod error;'.
- error.rs: unified OAKCORE_* codes; restore Error::new() and
  From<OcioError> from oak-common's error type.
- backend.rs/color.rs: oak_core::/oak_render:: self-references
  rewritten to crate::; the shaderfx-dependent GPU effect test moved
  to oak-render's shaderfx tests (shaderfx depends on oak-node and
  cannot live in oak-core).
- oak-render's error module re-exports oak_core::error::{Error,
  Result}; the OAKRENDER_* codes stay as the public-code contract.
- oak-node jobs.rs: ColorProcessor imported from oak_core::color.
- Integration tests repointed at oak_core::{texture, frame, backend,
  color, colormath}.
- the display-ICC regression test treats an empty OAK_DISPLAY_ICC as
  unset, matching displayicc::env_override_icc.
2026-09-03 17:42:20 +08:00

697 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! `ProxyTask`, mirroring `src/task/src/proxy/proxy.h`.
//!
//! Transcodes a video stream to a proxy via an external `ffmpeg` process
//! (oakcodec kind `OAKCODEC_TASK_PROXY`). The two pure functions
//! [`ProxyTask::build_arguments`] and [`ProxyTask::parse_progress`] are
//! parity/golden candidates (see tests/parity_test.rs).
//!
//! CPP-PARITY: src/task/src/proxy/proxy.h
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use oak_codec::proxymanager::ProxyManager;
use oak_codec::task::TaskRequest;
use crate::error::{Error, Result};
use crate::task::{Task, TaskBehavior};
/// The proxy parameters, mirroring `oakcodec_proxy_params` in
/// `include/codec/proxy.h` (kept as plain fields so no oakcodec handle is
/// needed to build arguments).
pub struct ProxyParams {
/// Target width (0 = unspecified/divider-based).
pub width: i32,
/// Target height (0 = unspecified/divider-based).
pub height: i32,
/// Resolution divider.
pub divider: i32,
/// Proxy format version.
pub version: i32,
/// CRF for the proxy encode.
pub crf: i32,
/// Whether audio is included in the proxy.
pub include_audio: bool,
/// Output container extension.
pub extension: String,
/// ffmpeg preset name.
pub preset: String,
}
/// A proxy transcode task: runs `ffmpeg` with arguments from
/// [`ProxyTask::build_arguments`], parsing progress lines via
/// [`ProxyTask::parse_progress`].
pub struct ProxyTask {
/// The shared task base.
pub base: Task,
/// Source media filename.
pub source_filename: String,
/// Target stream index.
pub stream_index: i32,
/// Proxy parameters.
pub params: ProxyParams,
/// Output proxy filename.
pub output_filename: String,
/// Total media duration in seconds (for progress scaling).
duration_seconds: f64,
}
/// 代理转码的编码器选择(按平台优先级探测到的第一个硬件编码器,
/// 否则软件 x264)。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HwEncoder {
/// macOS VideoToolbox`-c:v h264_videotoolbox`)。
VideoToolbox,
/// NVIDIA NVENCWindows/Linux`-c:v h264_nvenc`)。
Nvenc,
/// Intel QuickSync`-c:v h264_qsv`)。
Qsv,
/// AMD AMF`-c:v h264_amf`)。
Amf,
/// 软件 libx264C++ 路径)。
Software,
}
impl ProxyTask {
/// Build a proxy task from an oakcodec request and proxy params,
/// mirroring the C++ constructor (divider-based requests take the source
/// fraction).
pub fn new(request: &TaskRequest, params: ProxyParams) -> ProxyTask {
let mut params = params;
if request.proxy_width > 0 && request.proxy_height > 0 {
params.width = request.proxy_width;
params.height = request.proxy_height;
params.divider = 1;
}
let title = format!(
"Generating Proxy {}:{}",
request.input_filename, request.stream_index
);
ProxyTask {
base: Task::new(&title, None),
source_filename: request.input_filename.to_string(),
stream_index: request.stream_index,
params,
output_filename: request.output_filename.to_string(),
duration_seconds: 0.0,
}
}
/// Build the `ffmpeg` command-line argument vector for the proxy
/// transcode. Mirrors `build_arguments` in proxy.cpp; the exact ordering
/// and flag spelling are parity-locked (golden test).
///
/// CPP-PARITY: src/task/src/proxy/proxy.cpp (build_arguments)
pub fn build_arguments(
source_filename: &str,
stream_index: i32,
params: &ProxyParams,
output_filename: &str,
) -> Vec<String> {
let scale_filter = if params.divider > 1 {
format!(
"scale=w=trunc(iw/{}/2)*2:h=trunc(ih/{}/2)*2",
params.divider, params.divider
)
} else {
format!(
"scale=w={}:h={}:force_original_aspect_ratio=decrease",
params.width, params.height
)
};
let container_format = if params.extension.is_empty() {
"mp4".to_string()
} else {
params.extension.clone()
};
let mut args: Vec<String> = vec![
"-y".to_string(),
"-nostats".to_string(),
"-progress".to_string(),
"pipe:1".to_string(),
"-i".to_string(),
source_filename.to_string(),
"-map".to_string(),
format!("0:{stream_index}"),
];
if params.include_audio {
args.extend([
"-map".to_string(),
"0:a?".to_string(),
"-c:a".to_string(),
"aac".to_string(),
"-b:a".to_string(),
"128k".to_string(),
]);
} else {
args.push("-an".to_string());
}
args.extend([
"-vf".to_string(),
scale_filter,
"-c:v".to_string(),
"libx264".to_string(),
"-preset".to_string(),
params.preset.clone(),
"-crf".to_string(),
params.crf.to_string(),
"-pix_fmt".to_string(),
"yuv420p".to_string(),
"-movflags".to_string(),
"+faststart".to_string(),
"-f".to_string(),
container_format,
output_filename.to_string(),
]);
args
}
// ---- 硬件加速与资源控制 --------------------------------------------------
//
// build_arguments 是 C++ 纯软件路径(parity 锁定,不动);实际转码走
// [`ProxyTask::build_transcode_arguments`]:能用硬件编码器就一定用
// macOS VideoToolboxWindows/Linux 的 NVENC/QSV/AMF;探测不到回退
// libx264),解码侧 `-hwaccel auto`HEVC 4:2:2 10-bit 在
// Apple Silicon、RTX 50 系+、Intel 显卡上都能硬解;硬解失败 ffmpeg
// 自动回退软解)。
/// 探测本机可用的硬件 H.264 编码器(每进程每 ffmpeg 路径缓存一次:
/// `ffmpeg -hide_banner -encoders` 的输出里按平台优先级找)。
pub fn probe_hw_encoder(ffmpeg_path: &str) -> HwEncoder {
static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, HwEncoder>>> =
std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
let mut cache = cache.lock().unwrap_or_else(|e| e.into_inner());
if let Some(enc) = cache.get(ffmpeg_path) {
return *enc;
}
let enc = Self::probe_hw_encoder_uncached(ffmpeg_path);
cache.insert(ffmpeg_path.to_string(), enc);
enc
}
fn probe_hw_encoder_uncached(ffmpeg_path: &str) -> HwEncoder {
let out = Command::new(ffmpeg_path)
.args(["-hide_banner", "-encoders"])
.output();
let Ok(out) = out else {
return HwEncoder::Software;
};
let text = String::from_utf8_lossy(&out.stdout);
let has = |name: &str| text.contains(name);
// 平台优先级:macOS 只走 VideoToolboxWindows NVENC → QSV →
// AMFLinux NVENC → QSV → AMFVAAPI 需要 hwupload 帧上传链,
// 暂不支持,回退软件)。
let preference: &[(&str, HwEncoder)] = if cfg!(target_os = "macos") {
&[("h264_videotoolbox", HwEncoder::VideoToolbox)]
} else {
&[
("h264_nvenc", HwEncoder::Nvenc),
("h264_qsv", HwEncoder::Qsv),
("h264_amf", HwEncoder::Amf),
]
};
for (name, enc) in preference {
if has(name) {
return *enc;
}
}
HwEncoder::Software
}
/// libx264 预设名 → NVENC p1..p7NVENC 预设是编号)。
fn nvenc_preset(preset: &str) -> &'static str {
match preset {
"ultrafast" => "p1",
"superfast" => "p2",
"veryfast" => "p3",
"faster" => "p4",
"fast" => "p5",
"medium" => "p6",
_ => "p7",
}
}
/// libx264 预设名 → AMF quality 档。
fn amf_quality(preset: &str) -> &'static str {
match preset {
"ultrafast" | "superfast" | "veryfast" | "faster" | "fast" => "speed",
"medium" => "balanced",
_ => "quality",
}
}
/// The video-codec arguments for `enc` (quality mapped from the
/// proxy CRF: NVENC `-cq`、QSV `-global_quality`、AMF `-qp_i/-qp_p`
/// 与 CRF 同刻度直接用;VideoToolbox 用 qscale 语义 `-q:v`)。
fn hw_video_args(enc: HwEncoder, params: &ProxyParams) -> Vec<String> {
match enc {
HwEncoder::VideoToolbox => vec![
"-c:v".to_string(),
"h264_videotoolbox".to_string(),
"-q:v".to_string(),
params.crf.to_string(),
],
HwEncoder::Nvenc => vec![
"-c:v".to_string(),
"h264_nvenc".to_string(),
"-preset".to_string(),
Self::nvenc_preset(&params.preset).to_string(),
"-cq".to_string(),
params.crf.to_string(),
],
HwEncoder::Qsv => vec![
"-c:v".to_string(),
"h264_qsv".to_string(),
"-global_quality".to_string(),
params.crf.to_string(),
],
HwEncoder::Amf => vec![
"-c:v".to_string(),
"h264_amf".to_string(),
"-quality".to_string(),
Self::amf_quality(&params.preset).to_string(),
"-rc".to_string(),
"cqp".to_string(),
"-qp_i".to_string(),
params.crf.to_string(),
"-qp_p".to_string(),
params.crf.to_string(),
],
HwEncoder::Software => vec![
"-c:v".to_string(),
"libx264".to_string(),
"-preset".to_string(),
params.preset.clone(),
"-crf".to_string(),
params.crf.to_string(),
],
}
}
/// 实际转码用的参数:build_arguments 的软件路径 + 硬件加速与线程
/// 控制(解码 `-hwaccel auto`;编码按探测结果;`-threads` 限制
/// 解码/编码线程——后台任务不把机器打满)。
pub fn build_transcode_arguments(
source_filename: &str,
stream_index: i32,
params: &ProxyParams,
output_filename: &str,
enc: HwEncoder,
threads: i32,
) -> Vec<String> {
// 软件路径直接复用 parity 参数,仅在最前面加解码 hwaccel 与
// 线程数(不改变 C++ 的参数顺序语义之外的任何东西)。
let base = Self::build_arguments(source_filename, stream_index, params, output_filename);
let mut args: Vec<String> = vec!["-y".to_string(), "-nostats".to_string()];
if threads > 0 {
args.extend(["-threads".to_string(), threads.to_string()]);
}
args.extend(["-hwaccel".to_string(), "auto".to_string()]);
// base 以 -y -nostats 开头,跳过这两个,余下的按序接上;编码
// 器参数(-c:v 起)在 hw 时整段替换。
let mut rest = base[2..].to_vec();
if enc != HwEncoder::Software {
// 找到 "-c:v libx264" 段(-c:v 起到 -pix_fmt 前),换成 hw 参数。
if let Some(cv) = rest.iter().position(|a| a == "-c:v") {
let end = rest[cv..]
.iter()
.position(|a| a == "-pix_fmt")
.map(|p| cv + p)
.unwrap_or(rest.len());
rest.splice(cv..end, Self::hw_video_args(enc, params));
}
}
args.append(&mut rest);
args
}
/// 代理转码并发上限的配置键(Proxy Settings 对话框可调;
/// 默认 1——后台任务不给系统造成压力)。
pub const CONFIG_KEY_PROXY_MAX_CONCURRENT: &str = "ProxyMaxConcurrent";
/// 配置的代理转码并发上限([1, 16])。
pub fn proxy_max_concurrent() -> i32 {
oak_core::configstore::ConfigStore::instance()
.get_int(None, Self::CONFIG_KEY_PROXY_MAX_CONCURRENT, 1)
.clamp(1, 16)
}
/// 后台转码的线程预算(不变式:并发数 × 每任务线程数 ≤
/// 逻辑处理器数的一半——机器永远留一半以上给前台)。`concurrent`
/// 是当前的并发上限配置。
pub fn transcode_thread_budget(concurrent: i32) -> i32 {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2) as i32;
let half = (cores / 2).max(1);
(half / concurrent.max(1)).clamp(1, 8)
}
/// Parse a single `ffmpeg -progress` output line into a progress value in
/// 0.0..=1.0 given the total duration. Mirrors `parse_progress` in
/// proxy.cpp (golden test).
///
/// CPP-PARITY: src/task/src/proxy/proxy.cpp (parse_progress)
pub fn parse_progress(line: &str, duration_seconds: f64) -> Option<f64> {
if duration_seconds <= 0.0 {
return None;
}
let out_time_us: i64 = if let Some(rest) = line.strip_prefix("out_time_us=") {
rest.trim().parse().unwrap_or(-1)
} else if let Some(rest) = line.strip_prefix("out_time_ms=") {
// Despite the name, ffmpeg reports this value in microseconds.
rest.trim().parse().unwrap_or(-1)
} else {
-1
};
if out_time_us < 0 {
return None;
}
let progress = out_time_us as f64 / 1_000_000.0 / duration_seconds;
Some(if progress < 0.0 {
0.0
} else if progress > 1.0 {
1.0
} else {
progress
})
}
}
impl TaskBehavior for ProxyTask {
/// Spawn `ffmpeg` with the built arguments, feed `-progress` lines to
/// [`ProxyTask::parse_progress`] and emit them as task progress.
fn run(&mut self, task: &mut Task) -> Result<()> {
// Direct call into oakcodec's proxy manager (single-lib
// unification: the old two-stage C ABI getter is gone; an empty
// string means "not found"). The `FFmpegPath` config takes
// precedence when set (C++ `OAK_CONFIG("FFmpegPath")`).
let configured = oak_core::configstore::ConfigStore::instance()
.get(None, "FFmpegPath")
.unwrap_or_default();
let ffmpeg_path = ProxyManager::find_ffmpeg(&configured);
if ffmpeg_path.is_empty() {
task.set_error(
"Failed to generate proxy: ffmpeg executable was not found. Set the ffmpeg path in Preferences > Disk > Proxy Settings.",
);
return Err(Error::Failed("ffmpeg executable was not found".to_string()));
}
// Create the output directory if needed.
if let Some(parent) = std::path::Path::new(&self.output_filename).parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
if std::fs::create_dir_all(parent).is_err() {
task.set_error("Failed to create proxy output directory");
return Err(Error::Failed(
"Failed to create proxy output directory".to_string(),
));
}
}
}
let _ = std::fs::remove_file(&self.output_filename);
let working_filename = format!("{}.working.mp4", self.output_filename);
let _ = std::fs::remove_file(&working_filename);
// 硬件加速(能用硬编就一定用;探测不到回退 libx264)+ 线程
// 预算(后台任务不把机器打满)。
let encoder = Self::probe_hw_encoder(&ffmpeg_path);
let threads = Self::transcode_thread_budget(Self::proxy_max_concurrent());
let args = Self::build_transcode_arguments(
&self.source_filename,
self.stream_index,
&self.params,
&working_filename,
encoder,
threads,
);
// Probe the source duration for progress scaling (0 when unknown).
self.duration_seconds = probe_source_duration_seconds(&ffmpeg_path, &self.source_filename);
// Unix 上降优先级跑(nice 10:代理是后台任务,不能和前台
// 抢系统);Windows 没有 nice,线程预算已限制占用。
#[cfg(unix)]
let mut command = {
let mut c = Command::new("nice");
c.arg("-n").arg("10").arg(&ffmpeg_path);
c
};
#[cfg(not(unix))]
let mut command = Command::new(&ffmpeg_path);
command
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match command.spawn() {
Ok(child) => child,
Err(_) => {
task.set_error("Failed to start ffmpeg for proxy generation");
return Err(Error::Failed("Failed to start ffmpeg".to_string()));
}
};
// Drain stderr in a thread so a chatty ffmpeg cannot block us.
let stderr = child.stderr.take();
if let Some(stderr) = stderr {
std::thread::spawn(move || {
let _ = BufReader::new(stderr).lines().count();
});
}
let stdout = child.stdout.take();
let mut last_progress = 0.0_f64;
if let Some(stdout) = stdout {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(line) => {
if let Some(progress) = Self::parse_progress(&line, self.duration_seconds) {
if progress >= 0.0 && progress - last_progress > 0.001 {
last_progress = progress;
task.emit_progress(progress);
}
}
}
Err(_) => break,
}
if task.is_cancelled() {
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(&working_filename);
task.set_error("Proxy generation was cancelled");
return Err(Error::Cancelled);
}
}
}
let exit_status = child.wait();
if !matches!(exit_status, Ok(status) if status.success()) {
let _ = std::fs::remove_file(&working_filename);
task.set_error("ffmpeg failed to generate proxy");
return Err(Error::Failed("ffmpeg failed to generate proxy".to_string()));
}
if !std::path::Path::new(&working_filename).exists() {
task.set_error("ffmpeg finished but proxy file was not created");
return Err(Error::Failed(
"ffmpeg finished but proxy file was not created".to_string(),
));
}
if std::fs::rename(&working_filename, &self.output_filename).is_err() {
task.set_error("Failed to move proxy into place");
return Err(Error::Failed("Failed to move proxy into place".to_string()));
}
task.emit_progress(1.0);
Ok(())
}
}
/// Probe the source duration via `ffprobe` next to ffmpeg; 0.0 when
/// unavailable (in which case no intermediate progress is reported).
fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f64 {
let ffprobe = std::path::Path::new(ffmpeg_path).with_file_name("ffprobe");
if !ffprobe.exists() {
return 0.0;
}
let output = Command::new(&ffprobe)
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
source_filename,
])
.output();
match output {
Ok(out) if out.status.success() => {
let text = String::from_utf8_lossy(&out.stdout);
text.trim().parse().unwrap_or(0.0)
}
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn params() -> ProxyParams {
ProxyParams {
width: 1280,
height: 720,
divider: 1,
version: 1,
crf: 23,
include_audio: true,
extension: "mp4".to_string(),
preset: "veryfast".to_string(),
}
}
/// 软件路径 = parity 参数 + 解码 hwaccel/线程前缀;编码器参数
/// 原样(libx264 + preset + crf)。
#[test]
fn transcode_arguments_software_keeps_parity_body() {
let args = ProxyTask::build_transcode_arguments(
"/src.mov", 0, &params(), "/dst.mp4", HwEncoder::Software, 4,
);
assert!(args.windows(2).any(|w| w == ["-hwaccel", "auto"]));
assert!(args.windows(2).any(|w| w == ["-threads", "4"]));
assert!(args.windows(2).any(|w| w == ["-c:v", "libx264"]));
assert!(args.windows(2).any(|w| w == ["-crf", "23"]));
// -threads/-hwaccel 在 -i 之前(解码侧选项)。
let i_pos = args.iter().position(|a| a == "-i").unwrap();
let hw_pos = args.iter().position(|a| a == "-hwaccel").unwrap();
assert!(hw_pos < i_pos);
}
/// 硬件编码器整段替换 -c:v 段(不再出现 libx264/crf),各自的
/// 质量/预设映射正确。
#[test]
fn transcode_arguments_hw_swaps_encoder() {
let vt = ProxyTask::build_transcode_arguments(
"/src.mov", 0, &params(), "/dst.mp4", HwEncoder::VideoToolbox, 4,
);
assert!(vt.windows(2).any(|w| w == ["-c:v", "h264_videotoolbox"]));
assert!(vt.windows(2).any(|w| w == ["-q:v", "23"]));
assert!(!vt.iter().any(|a| a == "libx264" || a == "-crf"));
let nv = ProxyTask::build_transcode_arguments(
"/src.mov", 0, &params(), "/dst.mp4", HwEncoder::Nvenc, 4,
);
assert!(nv.windows(2).any(|w| w == ["-c:v", "h264_nvenc"]));
assert!(nv.windows(2).any(|w| w == ["-preset", "p3"]));
assert!(nv.windows(2).any(|w| w == ["-cq", "23"]));
let qsv = ProxyTask::build_transcode_arguments(
"/src.mov", 0, &params(), "/dst.mp4", HwEncoder::Qsv, 4,
);
assert!(qsv.windows(2).any(|w| w == ["-c:v", "h264_qsv"]));
assert!(qsv.windows(2).any(|w| w == ["-global_quality", "23"]));
let amf = ProxyTask::build_transcode_arguments(
"/src.mov", 0, &params(), "/dst.mp4", HwEncoder::Amf, 4,
);
assert!(amf.windows(2).any(|w| w == ["-c:v", "h264_amf"]));
assert!(amf.windows(2).any(|w| w == ["-quality", "speed"]));
// -pix_fmt/-movflags 等后续段在替换后仍然保留。
assert!(vt.windows(2).any(|w| w == ["-pix_fmt", "yuv420p"]));
}
/// 线程预算不变式:并发 × 线程 ≤ 逻辑处理器数一半;区间 [1, 8]。
#[test]
fn transcode_thread_budget_invariant() {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2) as i32;
let half = (cores / 2).max(1);
for concurrent in [1, 2, 4, 8, 16] {
let b = ProxyTask::transcode_thread_budget(concurrent);
assert!((1..=8).contains(&b));
assert!(
b * concurrent <= half.max(1) || b == 1,
"concurrency {concurrent} x threads {b} exceeds half of {cores} cores"
);
}
// 并发为 1 时独占一半核。
assert_eq!(ProxyTask::transcode_thread_budget(1), half.clamp(1, 8));
}
/// End-to-end: transcode a real 4K H.265 4:2:2 10-bit file (the
/// class of media the 4K playback lag was reported on) to a 720p
/// proxy. Skipped when the fixture or ffmpeg is missing.
#[test]
fn proxy_task_transcodes_hevc_422_10bit() {
let src = "/tmp/oakperf/hevc422-4k.mp4";
if !std::path::Path::new(src).exists() {
eprintln!("fixture missing; skipping");
return;
}
let ffmpeg = ProxyManager::find_ffmpeg("");
if ffmpeg.is_empty() {
eprintln!("ffmpeg not found; skipping");
return;
}
let out = "/tmp/oakperf/proxy-720p.mp4";
let _ = std::fs::remove_file(out);
let request = oak_codec::task::TaskRequest {
kind: oak_codec::task::TaskKind::Proxy,
input_filename: src,
output_filename: out,
stream_index: 0,
sample_rate: 0,
channel_layout: 0,
sample_format: 0,
proxy_width: 1280,
proxy_height: 720,
};
let params = ProxyParams {
width: 1280,
height: 720,
divider: 1,
version: 1,
crf: 23,
include_audio: true,
extension: "mp4".to_string(),
preset: "veryfast".to_string(),
};
let mut proxy_task = ProxyTask::new(&request, params);
let mut task = Task::new("probe", None);
proxy_task
.run(&mut task)
.expect("the proxy transcode succeeds on 4K HEVC 4:2:2 10-bit");
let meta = std::fs::metadata(out).expect("the proxy file exists");
assert!(meta.len() > 0, "the proxy file is non-empty");
}
}