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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user