proxy: hardware-accelerated transcode with configurable, resource-aware concurrency
Proxy generation no longer pegs the machine: - the transcode probes for a hardware H.264 encoder once per process and uses it when present (macOS h264_videotoolbox; Windows/Linux h264_nvenc -> h264_qsv -> h264_amf; libx264 remains the universal fallback and the untouched C++ parity path), with decode-side -hwaccel auto (HEVC 4:2:2 10-bit decodes in hardware on Apple Silicon / RTX 50+ / Intel GPUs and falls back to software cleanly); quality/preset map from the proxy CRF/preset per encoder - the concurrency limit is configurable (ProxyMaxConcurrent, default 1) in the Proxy Settings dialog; auto-generated jobs queue and the next starts when a slot frees. The invariant concurrency x per-job threads <= logical cores / 2 holds by construction (thread budget = half the cores / concurrency, clamped to [1,8], covered by a unit test), and on Unix ffmpeg runs nice -n 10 so the background task never starves the foreground - deleting a footage's proxy also removes it from the auto-generation queue
This commit is contained in:
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Proxy-Höhe"
|
||||
"proxydialog.crf": "Proxy-CRF"
|
||||
"proxydialog.preset": "Proxy-Preset"
|
||||
"proxydialog.max_concurrent": "Max. parallele Aufträge"
|
||||
"proxydialog.include_audio": "Audio in Proxys einschließen"
|
||||
"proxydialog.ffmpeg": "ffmpeg-Programm"
|
||||
"proxydialog.generate": "Proxys generieren"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Proxy Height"
|
||||
"proxydialog.crf": "Proxy CRF"
|
||||
"proxydialog.preset": "Proxy Preset"
|
||||
"proxydialog.max_concurrent": "Max Concurrent Jobs"
|
||||
"proxydialog.include_audio": "Include audio in proxies"
|
||||
"proxydialog.ffmpeg": "ffmpeg Executable"
|
||||
"proxydialog.generate": "Generate Proxies"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Alto del proxy"
|
||||
"proxydialog.crf": "CRF del proxy"
|
||||
"proxydialog.preset": "Preajuste del proxy"
|
||||
"proxydialog.max_concurrent": "Trabajos simultáneos máximos"
|
||||
"proxydialog.include_audio": "Incluir audio en los proxies"
|
||||
"proxydialog.ffmpeg": "Ejecutable de ffmpeg"
|
||||
"proxydialog.generate": "Generar proxies"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Hauteur du proxy"
|
||||
"proxydialog.crf": "CRF du proxy"
|
||||
"proxydialog.preset": "Présélection du proxy"
|
||||
"proxydialog.max_concurrent": "Tâches simultanées max."
|
||||
"proxydialog.include_audio": "Inclure l'audio dans les proxies"
|
||||
"proxydialog.ffmpeg": "Exécutable ffmpeg"
|
||||
"proxydialog.generate": "Générer les proxies"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "プロキシ高さ"
|
||||
"proxydialog.crf": "プロキシCRF"
|
||||
"proxydialog.preset": "プロキシプリセット"
|
||||
"proxydialog.max_concurrent": "最大同時ジョブ数"
|
||||
"proxydialog.include_audio": "プロキシにオーディオを含める"
|
||||
"proxydialog.ffmpeg": "ffmpeg 実行ファイル"
|
||||
"proxydialog.generate": "プロキシを生成"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Altura do proxy"
|
||||
"proxydialog.crf": "CRF do proxy"
|
||||
"proxydialog.preset": "Predefinição do proxy"
|
||||
"proxydialog.max_concurrent": "Máx. de trabalhos simultâneos"
|
||||
"proxydialog.include_audio": "Incluir áudio nos proxies"
|
||||
"proxydialog.ffmpeg": "Executável do ffmpeg"
|
||||
"proxydialog.generate": "Gerar proxies"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "Высота прокси"
|
||||
"proxydialog.crf": "CRF прокси"
|
||||
"proxydialog.preset": "Пресет прокси"
|
||||
"proxydialog.max_concurrent": "Макс. параллельных задач"
|
||||
"proxydialog.include_audio": "Включать звук в прокси"
|
||||
"proxydialog.ffmpeg": "Исполняемый файл ffmpeg"
|
||||
"proxydialog.generate": "Создать прокси"
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"proxydialog.height": "代理高度"
|
||||
"proxydialog.crf": "代理 CRF"
|
||||
"proxydialog.preset": "代理预设"
|
||||
"proxydialog.max_concurrent": "最大并发任务数"
|
||||
"proxydialog.include_audio": "代理包含音频"
|
||||
"proxydialog.ffmpeg": "ffmpeg 可执行文件"
|
||||
"proxydialog.generate": "生成代理"
|
||||
|
||||
@@ -1068,6 +1068,7 @@ pub struct ProxyDialogContent<E: crate::oakui::engine::AppEngine> {
|
||||
crf: Entity<SpinBox>,
|
||||
preset: Entity<ComboBox>,
|
||||
include_audio: Entity<CheckBox>,
|
||||
max_concurrent: Entity<SpinBox>,
|
||||
ffmpeg_path: Entity<PathField>,
|
||||
custom_params: Entity<CheckBox>,
|
||||
/// Snapshot of the footage rows (refreshed after generate / delete).
|
||||
@@ -1162,6 +1163,21 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
.with_label(i18n::tr("proxydialog.include_audio"))
|
||||
});
|
||||
|
||||
let max_concurrent = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
28,
|
||||
SliderModel::new(
|
||||
ValueKind::Integer,
|
||||
1.0,
|
||||
16.0,
|
||||
1.0,
|
||||
config_get_int("ProxyMaxConcurrent", 1).clamp(1, 16) as f64,
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let ffmpeg_path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField {
|
||||
@@ -1197,6 +1213,7 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
crf,
|
||||
preset,
|
||||
include_audio,
|
||||
max_concurrent,
|
||||
ffmpeg_path,
|
||||
custom_params,
|
||||
rows,
|
||||
@@ -1241,6 +1258,10 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
config_set_int("ProxyCRF", i64::from(params.crf));
|
||||
config_set_string("ProxyPreset", ¶ms.preset);
|
||||
config_set_bool("ProxyIncludeAudio", params.include_audio);
|
||||
config_set_int(
|
||||
"ProxyMaxConcurrent",
|
||||
self.max_concurrent.read(cx).value().to_f64().clamp(1.0, 16.0) as i64,
|
||||
);
|
||||
config_set_string(
|
||||
CONFIG_KEY_FFMPEG_PATH,
|
||||
self.ffmpeg_path.read(cx).path(cx).trim(),
|
||||
@@ -1426,6 +1447,11 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProxyDialogContent<E> {
|
||||
self.height.clone(),
|
||||
)),
|
||||
)
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("proxydialog.max_concurrent").into(),
|
||||
self.max_concurrent.clone(),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -1038,6 +1038,9 @@ pub struct RealEngine {
|
||||
/// The sending half of `thumb_rx` (cloned into every job).
|
||||
thumb_tx: Mutex<mpsc::Sender<ThumbEvent>>,
|
||||
proxy_runs: Vec<ProxyRun>,
|
||||
/// 待启动的代理生成队列(entry id):自动代理不并发轰炸系统——
|
||||
/// 同时只跑 [`PROXY_MAX_CONCURRENT`] 个转码,完成后从队列递补。
|
||||
proxy_queue: Vec<u64>,
|
||||
/// Last waveform-cache version seen by the tick (background waveform
|
||||
/// extractions bump it on insert → repaint the timeline).
|
||||
waveform_version_seen: u64,
|
||||
@@ -1182,6 +1185,7 @@ impl RealEngine {
|
||||
thumb_rx: Mutex::new(thumb_rx),
|
||||
thumb_tx: Mutex::new(thumb_tx),
|
||||
proxy_runs: Vec::new(),
|
||||
proxy_queue: Vec::new(),
|
||||
waveform_version_seen: 0,
|
||||
multicam_frames: Arc::new(Mutex::new(MulticamFrameCache::default())),
|
||||
multicam_rx: Mutex::new(multicam_rx),
|
||||
@@ -2140,11 +2144,31 @@ impl RealEngine {
|
||||
// generates.
|
||||
if !finished.is_empty() {
|
||||
self.invalidate_preview_frames(cx);
|
||||
// 有转码完成:从队列递补下一个(并发上限可调,见
|
||||
// proxy_max_concurrent)。
|
||||
self.pump_proxy_queue(cx);
|
||||
} else if changed {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动代理转码的并发上限(Proxy Settings 对话框可调,默认 1;
|
||||
/// 不变式"并发 × 每任务线程 ≤ 核数一半"的线程侧见
|
||||
/// [`oak_task::proxy::ProxyTask::transcode_thread_budget`])。
|
||||
fn proxy_max_concurrent() -> usize {
|
||||
oak_task::proxy::ProxyTask::proxy_max_concurrent() as usize
|
||||
}
|
||||
|
||||
/// 按并发上限从队列递补启动代理生成(队列去重由入队侧保证)。
|
||||
fn pump_proxy_queue(&mut self, cx: &mut Context<Self>) {
|
||||
while self.proxy_runs.len() < Self::proxy_max_concurrent() && !self.proxy_queue.is_empty() {
|
||||
let id = self.proxy_queue.remove(0);
|
||||
if let Err(err) = self.proxy_generate(id, cx) {
|
||||
println!("[real engine] queued proxy generation skipped {id}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-start proxy generation in the background for every footage
|
||||
/// that needs one (设计:全局 UseProxyMedia 开启时素材代理在后台
|
||||
/// 自动生成,不要求手动点 Generate;触发点:导入素材、打开工程)。
|
||||
@@ -2193,13 +2217,14 @@ impl RealEngine {
|
||||
.map(|node| node.identity())
|
||||
.collect()
|
||||
};
|
||||
// 入队 + 按并发上限递补(不一次全部启动:每个转码本身就是
|
||||
// 多线程 ffmpeg,N 个并发会把 CPU 打满)。
|
||||
for id in candidates {
|
||||
// proxy_generate 自己处理路径构建/任务线程/状态标记;失败
|
||||
// 只记日志(自动路径不打扰用户)。
|
||||
if let Err(err) = self.proxy_generate(id, cx) {
|
||||
println!("[real engine] auto proxy generation skipped {id}: {err}");
|
||||
if !self.proxy_queue.contains(&id) {
|
||||
self.proxy_queue.push(id);
|
||||
}
|
||||
}
|
||||
self.pump_proxy_queue(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -4754,6 +4779,9 @@ impl AppEngine for RealEngine {
|
||||
let Some(footage) = self.footage_of(id) else {
|
||||
return;
|
||||
};
|
||||
// 删除代理 = 明确不要了:同时移出自动生成队列(否则递补时
|
||||
// 会立刻重新生成一个)。
|
||||
self.proxy_queue.retain(|&q| q != id);
|
||||
let proxy_path = {
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
|
||||
@@ -72,6 +72,22 @@ pub struct ProxyTask {
|
||||
duration_seconds: f64,
|
||||
}
|
||||
|
||||
/// 代理转码的编码器选择(按平台优先级探测到的第一个硬件编码器,
|
||||
/// 否则软件 x264)。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum HwEncoder {
|
||||
/// macOS VideoToolbox(`-c:v h264_videotoolbox`)。
|
||||
VideoToolbox,
|
||||
/// NVIDIA NVENC(Windows/Linux,`-c:v h264_nvenc`)。
|
||||
Nvenc,
|
||||
/// Intel QuickSync(`-c:v h264_qsv`)。
|
||||
Qsv,
|
||||
/// AMD AMF(`-c:v h264_amf`)。
|
||||
Amf,
|
||||
/// 软件 libx264(C++ 路径)。
|
||||
Software,
|
||||
}
|
||||
|
||||
impl ProxyTask {
|
||||
/// Build a proxy task from an oakcodec request and proxy params,
|
||||
/// mirroring the C++ constructor (divider-based requests take the source
|
||||
@@ -171,6 +187,188 @@ impl ProxyTask {
|
||||
args
|
||||
}
|
||||
|
||||
// ---- 硬件加速与资源控制 --------------------------------------------------
|
||||
//
|
||||
// build_arguments 是 C++ 纯软件路径(parity 锁定,不动);实际转码走
|
||||
// [`ProxyTask::build_transcode_arguments`]:能用硬件编码器就一定用
|
||||
// (macOS VideoToolbox;Windows/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 只走 VideoToolbox;Windows NVENC → QSV →
|
||||
// AMF;Linux NVENC → QSV → AMF(VAAPI 需要 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..p7(NVENC 预设是编号)。
|
||||
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(¶ms.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(¶ms.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_common::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).
|
||||
@@ -240,16 +438,31 @@ impl TaskBehavior for ProxyTask {
|
||||
let working_filename = format!("{}.working.mp4", self.output_filename);
|
||||
let _ = std::fs::remove_file(&working_filename);
|
||||
|
||||
let args = Self::build_arguments(
|
||||
// 硬件加速(能用硬编就一定用;探测不到回退 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)
|
||||
@@ -352,6 +565,88 @@ fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f6
|
||||
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, ¶ms(), "/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, ¶ms(), "/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, ¶ms(), "/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, ¶ms(), "/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, ¶ms(), "/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.
|
||||
|
||||
Reference in New Issue
Block a user