app: auto-generate proxies in the background and badge them in the bin

With the global proxy switch on, footage that needs a proxy now gets
one generated in the background automatically — on import, on project
open, and when proxy use is enabled for a footage — instead of only
through the manual Generate menu action. Footage explicitly opted out
(a recorded proxy path with use disabled) and footage whose last
generation failed are not restarted; the generation path enables proxy
use so preview switches to the proxy as soon as it is ready.

The bin shows the lifecycle as a corner badge on each footage entry
(green P = ready, amber P = generating, red ! = failed; the widget
half lives in the gpui fork, submodule bumped here).

Includes a real-media test driving ProxyTask end-to-end on 4K H.265
4:2:2 10-bit (the media class the 4K playback lag was reported on):
180 s transcodes to a 720p proxy in ~25 s.
This commit is contained in:
2026-08-26 04:22:44 +08:00
parent 287a17399b
commit 09c7e7de32
3 changed files with 145 additions and 1 deletions
+92
View File
@@ -1895,6 +1895,23 @@ impl RealEngine {
self.clear_multicam_frames();
}
/// The bin entry's proxy badge (角标): the footage's proxy lifecycle
/// mapped onto the widget's badge enum; folders and footage without
/// proxy state get none.
fn proxy_badge_of(&self, id: u64) -> Option<gpui_widgets::project_explorer::ProxyBadge> {
use gpui_widgets::project_explorer::ProxyBadge;
let project = self.project.as_ref()?;
let node = graphops::id_of(id)?;
let guard = graphops::lock(project);
let f = graphops::footage_behavior(&guard.graph, node)?;
match self.proxy_state_of(f, node) {
super::engine::ProxyMediaState::Ready => Some(ProxyBadge::Ready),
super::engine::ProxyMediaState::Generating => Some(ProxyBadge::Generating),
super::engine::ProxyMediaState::Failed => Some(ProxyBadge::Failed),
super::engine::ProxyMediaState::Missing => None,
}
}
/// Attaches cached thumbnails to the bin entries, spawning a background
/// generation job for every footage that has none yet. Entries without a
/// renderable frame keep the widget's placeholder.
@@ -1908,6 +1925,12 @@ impl RealEngine {
if entry.is_dir {
return entry;
}
// The proxy badge rides every bin row (with or without a
// thumbnail), so a Ready proxy is visible at a glance.
let entry = match self.proxy_badge_of(entry.id) {
Some(badge) => entry.with_proxy_badge(badge),
None => entry,
};
let mut thumbs = self.thumbnails.lock().unwrap();
if let Some(path) = thumbs.done.get(&entry.id) {
return entry.with_thumbnail(path.to_string_lossy().into_owned());
@@ -2118,6 +2141,64 @@ impl RealEngine {
}
}
/// Auto-start proxy generation in the background for every footage
/// that needs one (设计:全局 UseProxyMedia 开启时素材代理在后台
/// 自动生成,不要求手动点 Generate;触发点:导入素材、打开工程)。
///
/// 逐素材规则:
/// - 已有 Ready 代理 / 已在生成 / 上次失败(state 3,避免失败
/// 循环)→ 跳过;
/// - 显式关过代理的素材(记录了代理路径但未启用)→ 跳过
/// (尊重每个素材的 opt-out);
/// - 其余(从未触碰过代理的,或已启用但未就绪的)→ 启动生成,
/// 生成路径会同时把 proxy_enabled 置位,完成后预览即走代理。
fn autostart_proxies(&mut self, cx: &mut Context<Self>) {
if !super::renderops::use_proxy_media() {
return;
}
let Some(project) = self.project.clone() else {
return;
};
let candidates: Vec<u64> = {
let guard = graphops::lock(&project);
graphops::footage_ids(&guard)
.into_iter()
.filter(|&node| {
let Some(f) = graphops::footage_behavior(&guard.graph, node) else {
return false;
};
if !f.streams.iter().any(|s| s.is_video) {
return false;
}
if f.proxy_state == 3 {
return false; // 上次失败:不自动重试
}
if !f.proxy.is_empty() {
if !f.proxy_enabled {
return false; // 显式关过代理
}
// 已启用:磁盘上就绪则无需再生成。
if oak_codec::proxymanager::ProxyManager::get_proxy_state(&f.proxy)
== oak_codec::proxymanager::ProxyState::Ready
{
return false;
}
}
self.proxy_runs.iter().all(|run| run.footage != node)
})
.map(|node| node.identity())
.collect()
};
for id in candidates {
// proxy_generate 自己处理路径构建/任务线程/状态标记;失败
// 只记日志(自动路径不打扰用户)。
if let Err(err) = self.proxy_generate(id, cx) {
println!("[real engine] auto proxy generation skipped {id}: {err}");
}
}
}
/// The proxy lifecycle state of one footage row: an in-flight run
/// wins, then the disk state of the recorded proxy path, with a
/// recorded failure (state 3) preserved while no file is on disk.
@@ -2565,6 +2646,10 @@ impl RealEngine {
// color pipeline from here on.
Self::apply_project_color_config(Some(&project));
// 全局代理开关开启时,工程里未就绪素材的代理在后台自动生成
// (打开长素材工程不等待:生成走任务线程)。
self.autostart_proxies(cx);
cx.notify();
}
@@ -4121,6 +4206,8 @@ impl AppEngine for RealEngine {
// The material bin reads the folder tree live from the graph, so a
// notify is enough for the explorer to list the new entry.
cx.notify();
// 全局代理开关开启时,新素材的代理在后台自动生成。
self.autostart_proxies(cx);
Ok(())
}
@@ -4695,6 +4782,11 @@ impl AppEngine for RealEngine {
f.proxy_enabled = enabled;
}
}
// 启用而代理未就绪时立即后台生成(禁用则是 opt-out,
// autostart_proxies 对有记录路径但未启用的素材不会重启)。
if enabled {
self.autostart_proxies(cx);
}
self.invalidate_preview_frames(cx);
}
+52
View File
@@ -347,3 +347,55 @@ fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f6
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 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");
}
}
+1 -1
Submodule gpui updated: 30aa052ca6...b6d3e70cbb