ci(lesavka): clear hygiene gate regressions
This commit is contained in:
parent
5b55c85263
commit
277442ef94
@ -4,6 +4,8 @@ include!("preview/feed_state.rs");
|
|||||||
include!("preview/feed_runtime.rs");
|
include!("preview/feed_runtime.rs");
|
||||||
include!("preview/status_pipeline.rs");
|
include!("preview/status_pipeline.rs");
|
||||||
include!("preview/frame_telemetry.rs");
|
include!("preview/frame_telemetry.rs");
|
||||||
|
include!("preview/feed_worker.rs");
|
||||||
|
include!("preview/launcher_preview_impl.rs");
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/preview.rs"]
|
#[path = "tests/preview.rs"]
|
||||||
|
|||||||
@ -224,315 +224,3 @@ impl PreviewFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn run_preview_feed(
|
|
||||||
server_addr: Arc<Mutex<String>>,
|
|
||||||
monitor_id: u32,
|
|
||||||
profile: PreviewProfile,
|
|
||||||
session_active: Arc<AtomicBool>,
|
|
||||||
active_bindings: Arc<AtomicUsize>,
|
|
||||||
running: Arc<AtomicBool>,
|
|
||||||
shared: Arc<Mutex<SharedPreviewState>>,
|
|
||||||
log_sink: Arc<Mutex<Option<std::sync::mpsc::Sender<String>>>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut startup_error = None;
|
|
||||||
let mut selected = None;
|
|
||||||
for decoder_name in preview_decoder_candidates() {
|
|
||||||
match build_preview_pipeline(profile, &decoder_name) {
|
|
||||||
Ok((pipeline, appsrc, appsink, decoder_label)) => {
|
|
||||||
match pipeline
|
|
||||||
.set_state(gst::State::Playing)
|
|
||||||
.context("starting launcher preview pipeline")
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
selected = Some((pipeline, appsrc, appsink, decoder_label));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = pipeline.set_state(gst::State::Null);
|
|
||||||
startup_error = Some(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
startup_error = Some(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (pipeline, appsrc, appsink, decoder_name) = selected.ok_or_else(|| {
|
|
||||||
startup_error.unwrap_or_else(|| anyhow::anyhow!("no usable H.264 decoder"))
|
|
||||||
})?;
|
|
||||||
let parser = pipeline.by_name("preview_parse");
|
|
||||||
let decoder = pipeline.by_name("decoder");
|
|
||||||
if let Ok(mut slot) = shared.lock() {
|
|
||||||
slot.telemetry.note_decoder(&decoder_name);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let shared = Arc::clone(&shared);
|
|
||||||
pipeline.connect_deep_element_added(move |_, _, element| {
|
|
||||||
if let Some(decoder_label) = preview_decoder_label(element)
|
|
||||||
&& let Ok(mut slot) = shared.lock()
|
|
||||||
{
|
|
||||||
slot.telemetry.note_decoder(&decoder_label);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let sample_worker = {
|
|
||||||
let shared = Arc::clone(&shared);
|
|
||||||
let appsink = appsink.clone();
|
|
||||||
let parser = parser.clone();
|
|
||||||
let decoder = decoder.clone();
|
|
||||||
let running = Arc::clone(&running);
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
if !running.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
|
|
||||||
if let Some(parser) = parser.as_ref() {
|
|
||||||
record_preview_caps(&shared, parser, "src", PreviewCapsKind::Stream);
|
|
||||||
}
|
|
||||||
if let Some(decoder) = decoder.as_ref() {
|
|
||||||
record_preview_caps(&shared, decoder, "src", PreviewCapsKind::Decoded);
|
|
||||||
}
|
|
||||||
if let Some(caps) = sample.caps() {
|
|
||||||
let caps_label = preview_caps_summary(&caps);
|
|
||||||
if !caps_label.is_empty()
|
|
||||||
&& let Ok(mut slot) = shared.lock()
|
|
||||||
{
|
|
||||||
slot.telemetry.note_rendered_caps(&caps_label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(frame) = sample_to_frame(&sample)
|
|
||||||
&& let Ok(mut slot) = shared.lock() {
|
|
||||||
slot.push_frame(frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.context("building preview tokio runtime")?;
|
|
||||||
|
|
||||||
let running_for_loop = Arc::clone(&running);
|
|
||||||
let _ = rt.block_on(async move {
|
|
||||||
let mut was_active = false;
|
|
||||||
let mut retry_delay = Duration::from_millis(750);
|
|
||||||
loop {
|
|
||||||
if !running_for_loop.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let active_now = session_active.load(Ordering::Relaxed)
|
|
||||||
&& active_bindings.load(Ordering::Relaxed) > 0;
|
|
||||||
if !active_now {
|
|
||||||
was_active = false;
|
|
||||||
retry_delay = Duration::from_millis(750);
|
|
||||||
set_shared_status(&shared, &log_sink, monitor_id, PREVIEW_IDLE_STATUS, true);
|
|
||||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !was_active {
|
|
||||||
was_active = true;
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Waking relay preview...",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
tokio::time::sleep(Duration::from_millis(350)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Connecting relay preview...",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let current_addr = match server_addr.lock() {
|
|
||||||
Ok(value) => value.clone(),
|
|
||||||
Err(_) => {
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview address is unavailable.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
tokio::time::sleep(Duration::from_millis(750)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let channel = match crate::relay_transport::endpoint(¤t_addr) {
|
|
||||||
Ok(endpoint) => match endpoint.tcp_nodelay(true).connect().await {
|
|
||||||
Ok(channel) => channel,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(monitor_id, ?err, "launcher preview connect failed");
|
|
||||||
log_preview_issue(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
&format!("Preview host is unavailable: {err}"),
|
|
||||||
);
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview host is unavailable.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
tokio::time::sleep(retry_delay).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
warn!(monitor_id, ?err, "launcher preview endpoint invalid");
|
|
||||||
log_preview_issue(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
&format!("Preview address is invalid: {err}"),
|
|
||||||
);
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview address is invalid.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
tokio::time::sleep(retry_delay).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cli = RelayClient::new(channel);
|
|
||||||
let req = MonitorRequest {
|
|
||||||
id: monitor_id,
|
|
||||||
max_bitrate: profile.max_bitrate_kbit,
|
|
||||||
requested_width: profile.requested_width.max(0) as u32,
|
|
||||||
requested_height: profile.requested_height.max(0) as u32,
|
|
||||||
requested_fps: profile.requested_fps,
|
|
||||||
source_id: Some(profile.source_monitor_id),
|
|
||||||
};
|
|
||||||
match cli.capture_video(Request::new(req)).await {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
retry_delay = Duration::from_millis(750);
|
|
||||||
debug!(monitor_id, "launcher preview connected");
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Waiting for stream...",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
loop {
|
|
||||||
if !session_active.load(Ordering::Relaxed)
|
|
||||||
|| !running_for_loop.load(Ordering::Relaxed)
|
|
||||||
|| active_bindings.load(Ordering::Relaxed) == 0
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match tokio::time::timeout(
|
|
||||||
Duration::from_millis(300),
|
|
||||||
stream.get_mut().message(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(Some(pkt))) => {
|
|
||||||
record_preview_packet(&shared, &pkt);
|
|
||||||
push_preview_packet(&appsrc, pkt);
|
|
||||||
}
|
|
||||||
Ok(Ok(None)) => {
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview stream ended.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
retry_delay = Duration::from_millis(1_500);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
warn!(monitor_id, ?err, "launcher preview stream error");
|
|
||||||
log_preview_issue(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
&format!("Preview stream error: {err}"),
|
|
||||||
);
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview stream error. See session log.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
retry_delay =
|
|
||||||
preview_retry_delay(retry_delay, Some(&err.to_string()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
if preview_startup_condition(&err) {
|
|
||||||
debug!(
|
|
||||||
monitor_id,
|
|
||||||
?err,
|
|
||||||
"launcher preview waiting for capture pipeline"
|
|
||||||
);
|
|
||||||
log_preview_issue(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
&format!("Waiting for capture pipeline: {err}"),
|
|
||||||
);
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Waiting for capture pipeline...",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
retry_delay = preview_retry_delay(retry_delay, Some(err.message()));
|
|
||||||
} else {
|
|
||||||
warn!(monitor_id, ?err, "launcher preview rpc failed");
|
|
||||||
log_preview_issue(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
&format!("Preview RPC failed: {err}"),
|
|
||||||
);
|
|
||||||
set_shared_status(
|
|
||||||
&shared,
|
|
||||||
&log_sink,
|
|
||||||
monitor_id,
|
|
||||||
"Preview RPC failed. See session log.",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
retry_delay = preview_retry_delay(retry_delay, Some(err.message()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokio::time::sleep(retry_delay).await;
|
|
||||||
}
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
Ok::<(), anyhow::Error>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
let _ = pipeline.set_state(gst::State::Null);
|
|
||||||
running.store(false, Ordering::Relaxed);
|
|
||||||
let _ = sample_worker.join();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
312
client/src/launcher/preview/feed_worker.rs
Normal file
312
client/src/launcher/preview/feed_worker.rs
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
#[cfg(not(coverage))]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn run_preview_feed(
|
||||||
|
server_addr: Arc<Mutex<String>>,
|
||||||
|
monitor_id: u32,
|
||||||
|
profile: PreviewProfile,
|
||||||
|
session_active: Arc<AtomicBool>,
|
||||||
|
active_bindings: Arc<AtomicUsize>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
shared: Arc<Mutex<SharedPreviewState>>,
|
||||||
|
log_sink: Arc<Mutex<Option<std::sync::mpsc::Sender<String>>>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut startup_error = None;
|
||||||
|
let mut selected = None;
|
||||||
|
for decoder_name in preview_decoder_candidates() {
|
||||||
|
match build_preview_pipeline(profile, &decoder_name) {
|
||||||
|
Ok((pipeline, appsrc, appsink, decoder_label)) => {
|
||||||
|
match pipeline
|
||||||
|
.set_state(gst::State::Playing)
|
||||||
|
.context("starting launcher preview pipeline")
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
selected = Some((pipeline, appsrc, appsink, decoder_label));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = pipeline.set_state(gst::State::Null);
|
||||||
|
startup_error = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
startup_error = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (pipeline, appsrc, appsink, decoder_name) = selected.ok_or_else(|| {
|
||||||
|
startup_error.unwrap_or_else(|| anyhow::anyhow!("no usable H.264 decoder"))
|
||||||
|
})?;
|
||||||
|
let parser = pipeline.by_name("preview_parse");
|
||||||
|
let decoder = pipeline.by_name("decoder");
|
||||||
|
if let Ok(mut slot) = shared.lock() {
|
||||||
|
slot.telemetry.note_decoder(&decoder_name);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let shared = Arc::clone(&shared);
|
||||||
|
pipeline.connect_deep_element_added(move |_, _, element| {
|
||||||
|
if let Some(decoder_label) = preview_decoder_label(element)
|
||||||
|
&& let Ok(mut slot) = shared.lock()
|
||||||
|
{
|
||||||
|
slot.telemetry.note_decoder(&decoder_label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let sample_worker = {
|
||||||
|
let shared = Arc::clone(&shared);
|
||||||
|
let appsink = appsink.clone();
|
||||||
|
let parser = parser.clone();
|
||||||
|
let decoder = decoder.clone();
|
||||||
|
let running = Arc::clone(&running);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
if !running.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
|
||||||
|
if let Some(parser) = parser.as_ref() {
|
||||||
|
record_preview_caps(&shared, parser, "src", PreviewCapsKind::Stream);
|
||||||
|
}
|
||||||
|
if let Some(decoder) = decoder.as_ref() {
|
||||||
|
record_preview_caps(&shared, decoder, "src", PreviewCapsKind::Decoded);
|
||||||
|
}
|
||||||
|
if let Some(caps) = sample.caps() {
|
||||||
|
let caps_label = preview_caps_summary(&caps);
|
||||||
|
if !caps_label.is_empty()
|
||||||
|
&& let Ok(mut slot) = shared.lock()
|
||||||
|
{
|
||||||
|
slot.telemetry.note_rendered_caps(&caps_label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(frame) = sample_to_frame(&sample)
|
||||||
|
&& let Ok(mut slot) = shared.lock()
|
||||||
|
{
|
||||||
|
slot.push_frame(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.context("building preview tokio runtime")?;
|
||||||
|
|
||||||
|
let running_for_loop = Arc::clone(&running);
|
||||||
|
let _ = rt.block_on(async move {
|
||||||
|
let mut was_active = false;
|
||||||
|
let mut retry_delay = Duration::from_millis(750);
|
||||||
|
loop {
|
||||||
|
if !running_for_loop.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let active_now =
|
||||||
|
session_active.load(Ordering::Relaxed) && active_bindings.load(Ordering::Relaxed) > 0;
|
||||||
|
if !active_now {
|
||||||
|
was_active = false;
|
||||||
|
retry_delay = Duration::from_millis(750);
|
||||||
|
set_shared_status(&shared, &log_sink, monitor_id, PREVIEW_IDLE_STATUS, true);
|
||||||
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !was_active {
|
||||||
|
was_active = true;
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Waking relay preview...",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tokio::time::sleep(Duration::from_millis(350)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Connecting relay preview...",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let current_addr = match server_addr.lock() {
|
||||||
|
Ok(value) => value.clone(),
|
||||||
|
Err(_) => {
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview address is unavailable.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tokio::time::sleep(Duration::from_millis(750)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel = match crate::relay_transport::endpoint(¤t_addr) {
|
||||||
|
Ok(endpoint) => match endpoint.tcp_nodelay(true).connect().await {
|
||||||
|
Ok(channel) => channel,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(monitor_id, ?err, "launcher preview connect failed");
|
||||||
|
log_preview_issue(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
&format!("Preview host is unavailable: {err}"),
|
||||||
|
);
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview host is unavailable.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tokio::time::sleep(retry_delay).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(monitor_id, ?err, "launcher preview endpoint invalid");
|
||||||
|
log_preview_issue(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
&format!("Preview address is invalid: {err}"),
|
||||||
|
);
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview address is invalid.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tokio::time::sleep(retry_delay).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cli = RelayClient::new(channel);
|
||||||
|
let req = MonitorRequest {
|
||||||
|
id: monitor_id,
|
||||||
|
max_bitrate: profile.max_bitrate_kbit,
|
||||||
|
requested_width: profile.requested_width.max(0) as u32,
|
||||||
|
requested_height: profile.requested_height.max(0) as u32,
|
||||||
|
requested_fps: profile.requested_fps,
|
||||||
|
source_id: Some(profile.source_monitor_id),
|
||||||
|
};
|
||||||
|
match cli.capture_video(Request::new(req)).await {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
retry_delay = Duration::from_millis(750);
|
||||||
|
debug!(monitor_id, "launcher preview connected");
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Waiting for stream...",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
loop {
|
||||||
|
if !session_active.load(Ordering::Relaxed)
|
||||||
|
|| !running_for_loop.load(Ordering::Relaxed)
|
||||||
|
|| active_bindings.load(Ordering::Relaxed) == 0
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::time::timeout(
|
||||||
|
Duration::from_millis(300),
|
||||||
|
stream.get_mut().message(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(Some(pkt))) => {
|
||||||
|
record_preview_packet(&shared, &pkt);
|
||||||
|
push_preview_packet(&appsrc, pkt);
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => {
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview stream ended.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
retry_delay = Duration::from_millis(1_500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
warn!(monitor_id, ?err, "launcher preview stream error");
|
||||||
|
log_preview_issue(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
&format!("Preview stream error: {err}"),
|
||||||
|
);
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview stream error. See session log.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
retry_delay =
|
||||||
|
preview_retry_delay(retry_delay, Some(&err.to_string()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if preview_startup_condition(&err) {
|
||||||
|
debug!(
|
||||||
|
monitor_id,
|
||||||
|
?err,
|
||||||
|
"launcher preview waiting for capture pipeline"
|
||||||
|
);
|
||||||
|
log_preview_issue(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
&format!("Waiting for capture pipeline: {err}"),
|
||||||
|
);
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Waiting for capture pipeline...",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
retry_delay = preview_retry_delay(retry_delay, Some(err.message()));
|
||||||
|
} else {
|
||||||
|
warn!(monitor_id, ?err, "launcher preview rpc failed");
|
||||||
|
log_preview_issue(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
&format!("Preview RPC failed: {err}"),
|
||||||
|
);
|
||||||
|
set_shared_status(
|
||||||
|
&shared,
|
||||||
|
&log_sink,
|
||||||
|
monitor_id,
|
||||||
|
"Preview RPC failed. See session log.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
retry_delay = preview_retry_delay(retry_delay, Some(err.message()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(retry_delay).await;
|
||||||
|
}
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = pipeline.set_state(gst::State::Null);
|
||||||
|
running.store(false, Ordering::Relaxed);
|
||||||
|
let _ = sample_worker.join();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
334
client/src/launcher/preview/launcher_preview_impl.rs
Normal file
334
client/src/launcher/preview/launcher_preview_impl.rs
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
#[cfg(not(coverage))]
|
||||||
|
impl LauncherPreview {
|
||||||
|
pub fn new(server_addr: String) -> Result<Self> {
|
||||||
|
gst::init().context("initialising preview gstreamer")?;
|
||||||
|
let server_addr = Arc::new(Mutex::new(server_addr));
|
||||||
|
let log_sink = Arc::new(Mutex::new(None));
|
||||||
|
let inline_feeds = Arc::new(Mutex::new([
|
||||||
|
PreviewFeed::spawn(
|
||||||
|
Arc::clone(&server_addr),
|
||||||
|
0,
|
||||||
|
PreviewSurface::Inline.profile(),
|
||||||
|
Arc::clone(&log_sink),
|
||||||
|
)?,
|
||||||
|
PreviewFeed::spawn(
|
||||||
|
Arc::clone(&server_addr),
|
||||||
|
1,
|
||||||
|
PreviewSurface::Inline.profile(),
|
||||||
|
Arc::clone(&log_sink),
|
||||||
|
)?,
|
||||||
|
]));
|
||||||
|
let window_feeds = Arc::new(Mutex::new([
|
||||||
|
PreviewFeed::spawn(
|
||||||
|
Arc::clone(&server_addr),
|
||||||
|
0,
|
||||||
|
PreviewSurface::Window.profile(),
|
||||||
|
Arc::clone(&log_sink),
|
||||||
|
)?,
|
||||||
|
PreviewFeed::spawn(
|
||||||
|
Arc::clone(&server_addr),
|
||||||
|
1,
|
||||||
|
PreviewSurface::Window.profile(),
|
||||||
|
Arc::clone(&log_sink),
|
||||||
|
)?,
|
||||||
|
]));
|
||||||
|
Ok(Self {
|
||||||
|
server_addr: Arc::clone(&server_addr),
|
||||||
|
log_sink: Arc::clone(&log_sink),
|
||||||
|
inline_feeds,
|
||||||
|
window_feeds,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_log_sink(&self, tx: std::sync::mpsc::Sender<String>) {
|
||||||
|
if let Ok(mut slot) = self.log_sink.lock() {
|
||||||
|
*slot = Some(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_server_addr(&self, server_addr: String) {
|
||||||
|
if let Ok(mut slot) = self.server_addr.lock() {
|
||||||
|
*slot = server_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_session_active(&self, active: bool) {
|
||||||
|
if let Ok(feeds) = self.inline_feeds.lock() {
|
||||||
|
for feed in feeds.iter() {
|
||||||
|
feed.set_active(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(feeds) = self.window_feeds.lock() {
|
||||||
|
for feed in feeds.iter() {
|
||||||
|
feed.set_active(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown_all(&self) {
|
||||||
|
if let Ok(feeds) = self.inline_feeds.lock() {
|
||||||
|
for feed in feeds.iter() {
|
||||||
|
feed.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(feeds) = self.window_feeds.lock() {
|
||||||
|
for feed in feeds.iter() {
|
||||||
|
feed.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_on_picture(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
surface: PreviewSurface,
|
||||||
|
picture: >k::Picture,
|
||||||
|
status_label: >k::Label,
|
||||||
|
) -> Option<PreviewBinding> {
|
||||||
|
match surface {
|
||||||
|
PreviewSurface::Inline => self
|
||||||
|
.inline_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.map(|feed| feed.install_on_picture(picture, status_label)),
|
||||||
|
PreviewSurface::Window => self
|
||||||
|
.window_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.map(|feed| feed.install_on_picture(picture, status_label)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot_metrics(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
surface: PreviewSurface,
|
||||||
|
) -> Option<PreviewMetricsSnapshot> {
|
||||||
|
match surface {
|
||||||
|
PreviewSurface::Inline => self
|
||||||
|
.inline_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.map(|feed| feed.snapshot_metrics()),
|
||||||
|
PreviewSurface::Window => self
|
||||||
|
.window_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.map(|feed| feed.snapshot_metrics()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_recording_tap(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
surface: PreviewSurface,
|
||||||
|
) -> Option<PreviewRecordingTap> {
|
||||||
|
match surface {
|
||||||
|
PreviewSurface::Inline => self
|
||||||
|
.inline_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.and_then(|feed| feed.start_recording_tap()),
|
||||||
|
PreviewSurface::Window => self
|
||||||
|
.window_feeds
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
||||||
|
.and_then(|feed| feed.start_recording_tap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_capture_profile(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
source_monitor_id: usize,
|
||||||
|
requested_width: i32,
|
||||||
|
requested_height: i32,
|
||||||
|
requested_fps: u32,
|
||||||
|
max_bitrate_kbit: u32,
|
||||||
|
) {
|
||||||
|
let (
|
||||||
|
inline_requested_width,
|
||||||
|
inline_requested_height,
|
||||||
|
inline_requested_fps,
|
||||||
|
inline_max_bitrate_kbit,
|
||||||
|
) = sanitize_preview_request(
|
||||||
|
requested_width,
|
||||||
|
requested_height,
|
||||||
|
requested_fps,
|
||||||
|
max_bitrate_kbit,
|
||||||
|
);
|
||||||
|
self.rebuild_feed(
|
||||||
|
&self.inline_feeds,
|
||||||
|
monitor_id,
|
||||||
|
Some((
|
||||||
|
source_monitor_id,
|
||||||
|
inline_requested_width,
|
||||||
|
inline_requested_height,
|
||||||
|
inline_requested_fps,
|
||||||
|
inline_max_bitrate_kbit,
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
self.rebuild_feed(
|
||||||
|
&self.window_feeds,
|
||||||
|
monitor_id,
|
||||||
|
Some((
|
||||||
|
source_monitor_id,
|
||||||
|
requested_width,
|
||||||
|
requested_height,
|
||||||
|
requested_fps,
|
||||||
|
max_bitrate_kbit,
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_breakout_profile(&self, monitor_id: usize, width: i32, height: i32) {
|
||||||
|
self.rebuild_feed(&self.window_feeds, monitor_id, None, Some((width, height)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn profile_for_test(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
surface: PreviewSurface,
|
||||||
|
) -> Option<(u32, i32, i32, i32, i32, u32, u32)> {
|
||||||
|
let feed = match surface {
|
||||||
|
PreviewSurface::Inline => self.inline_feeds.lock().ok()?.get(monitor_id).cloned(),
|
||||||
|
PreviewSurface::Window => self.window_feeds.lock().ok()?.get(monitor_id).cloned(),
|
||||||
|
}?;
|
||||||
|
let profile = feed.profile();
|
||||||
|
Some((
|
||||||
|
profile.source_monitor_id,
|
||||||
|
profile.display_width,
|
||||||
|
profile.display_height,
|
||||||
|
profile.requested_width,
|
||||||
|
profile.requested_height,
|
||||||
|
profile.requested_fps,
|
||||||
|
profile.max_bitrate_kbit,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn feed_disabled_for_test(
|
||||||
|
&self,
|
||||||
|
monitor_id: usize,
|
||||||
|
surface: PreviewSurface,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let feed = match surface {
|
||||||
|
PreviewSurface::Inline => self.inline_feeds.lock().ok()?.get(monitor_id).cloned(),
|
||||||
|
PreviewSurface::Window => self.window_feeds.lock().ok()?.get(monitor_id).cloned(),
|
||||||
|
}?;
|
||||||
|
Some(feed.is_disabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_feed(
|
||||||
|
&self,
|
||||||
|
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
|
||||||
|
monitor_id: usize,
|
||||||
|
requested: Option<(usize, i32, i32, u32, u32)>,
|
||||||
|
display: Option<(i32, i32)>,
|
||||||
|
) {
|
||||||
|
let Ok(mut feeds) = feeds.lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(existing) = feeds.get(monitor_id).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let was_active = existing.is_active();
|
||||||
|
let keep_disabled = existing.is_disabled();
|
||||||
|
let mut profile = existing.profile();
|
||||||
|
if let Some((
|
||||||
|
source_monitor_id,
|
||||||
|
requested_width,
|
||||||
|
requested_height,
|
||||||
|
requested_fps,
|
||||||
|
max_bitrate_kbit,
|
||||||
|
)) = requested
|
||||||
|
{
|
||||||
|
profile.source_monitor_id = source_monitor_id as u32;
|
||||||
|
profile.requested_width = requested_width.max(2);
|
||||||
|
profile.requested_height = requested_height.max(2);
|
||||||
|
profile.requested_fps = requested_fps.max(1);
|
||||||
|
profile.max_bitrate_kbit = max_bitrate_kbit.max(800);
|
||||||
|
}
|
||||||
|
if let Some((display_width, display_height)) = display {
|
||||||
|
profile.display_width = display_width.max(2);
|
||||||
|
profile.display_height = display_height.max(2);
|
||||||
|
}
|
||||||
|
let next_feed = if keep_disabled {
|
||||||
|
Some(PreviewFeed::spawn_disabled(profile))
|
||||||
|
} else {
|
||||||
|
match PreviewFeed::spawn(
|
||||||
|
Arc::clone(&self.server_addr),
|
||||||
|
monitor_id as u32,
|
||||||
|
profile,
|
||||||
|
Arc::clone(&self.log_sink),
|
||||||
|
) {
|
||||||
|
Ok(feed) => Some(feed),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(monitor_id, ?err, "could not rebuild preview feed");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(feed) = next_feed {
|
||||||
|
if was_active {
|
||||||
|
feed.set_active(true);
|
||||||
|
}
|
||||||
|
existing.shutdown();
|
||||||
|
feeds[monitor_id] = feed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_monitor_enabled(&self, monitor_id: usize, enabled: bool) {
|
||||||
|
self.set_feed_enabled(&self.inline_feeds, monitor_id, enabled);
|
||||||
|
self.set_feed_enabled(&self.window_feeds, monitor_id, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_feed_enabled(
|
||||||
|
&self,
|
||||||
|
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
|
||||||
|
monitor_id: usize,
|
||||||
|
enabled: bool,
|
||||||
|
) {
|
||||||
|
let Ok(mut feeds) = feeds.lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(existing) = feeds.get(monitor_id).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if existing.is_disabled() != enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let was_active = existing.is_active();
|
||||||
|
let profile = existing.profile();
|
||||||
|
let replacement = if enabled {
|
||||||
|
match PreviewFeed::spawn(
|
||||||
|
Arc::clone(&self.server_addr),
|
||||||
|
monitor_id as u32,
|
||||||
|
profile,
|
||||||
|
Arc::clone(&self.log_sink),
|
||||||
|
) {
|
||||||
|
Ok(feed) => feed,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(monitor_id, ?err, "could not enable preview feed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PreviewFeed::spawn_disabled(profile)
|
||||||
|
};
|
||||||
|
if was_active {
|
||||||
|
replacement.set_active(true);
|
||||||
|
}
|
||||||
|
existing.shutdown();
|
||||||
|
feeds[monitor_id] = replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -198,338 +198,3 @@ impl PreviewSurface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
|
||||||
impl LauncherPreview {
|
|
||||||
pub fn new(server_addr: String) -> Result<Self> {
|
|
||||||
gst::init().context("initialising preview gstreamer")?;
|
|
||||||
let server_addr = Arc::new(Mutex::new(server_addr));
|
|
||||||
let log_sink = Arc::new(Mutex::new(None));
|
|
||||||
let inline_feeds = Arc::new(Mutex::new([
|
|
||||||
PreviewFeed::spawn(
|
|
||||||
Arc::clone(&server_addr),
|
|
||||||
0,
|
|
||||||
PreviewSurface::Inline.profile(),
|
|
||||||
Arc::clone(&log_sink),
|
|
||||||
)?,
|
|
||||||
PreviewFeed::spawn(
|
|
||||||
Arc::clone(&server_addr),
|
|
||||||
1,
|
|
||||||
PreviewSurface::Inline.profile(),
|
|
||||||
Arc::clone(&log_sink),
|
|
||||||
)?,
|
|
||||||
]));
|
|
||||||
let window_feeds = Arc::new(Mutex::new([
|
|
||||||
PreviewFeed::spawn(
|
|
||||||
Arc::clone(&server_addr),
|
|
||||||
0,
|
|
||||||
PreviewSurface::Window.profile(),
|
|
||||||
Arc::clone(&log_sink),
|
|
||||||
)?,
|
|
||||||
PreviewFeed::spawn(
|
|
||||||
Arc::clone(&server_addr),
|
|
||||||
1,
|
|
||||||
PreviewSurface::Window.profile(),
|
|
||||||
Arc::clone(&log_sink),
|
|
||||||
)?,
|
|
||||||
]));
|
|
||||||
Ok(Self {
|
|
||||||
server_addr: Arc::clone(&server_addr),
|
|
||||||
log_sink: Arc::clone(&log_sink),
|
|
||||||
inline_feeds,
|
|
||||||
window_feeds,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_log_sink(&self, tx: std::sync::mpsc::Sender<String>) {
|
|
||||||
if let Ok(mut slot) = self.log_sink.lock() {
|
|
||||||
*slot = Some(tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_server_addr(&self, server_addr: String) {
|
|
||||||
if let Ok(mut slot) = self.server_addr.lock() {
|
|
||||||
*slot = server_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_session_active(&self, active: bool) {
|
|
||||||
if let Ok(feeds) = self.inline_feeds.lock() {
|
|
||||||
for feed in feeds.iter() {
|
|
||||||
feed.set_active(active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(feeds) = self.window_feeds.lock() {
|
|
||||||
for feed in feeds.iter() {
|
|
||||||
feed.set_active(active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown_all(&self) {
|
|
||||||
if let Ok(feeds) = self.inline_feeds.lock() {
|
|
||||||
for feed in feeds.iter() {
|
|
||||||
feed.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(feeds) = self.window_feeds.lock() {
|
|
||||||
for feed in feeds.iter() {
|
|
||||||
feed.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn install_on_picture(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
surface: PreviewSurface,
|
|
||||||
picture: >k::Picture,
|
|
||||||
status_label: >k::Label,
|
|
||||||
) -> Option<PreviewBinding> {
|
|
||||||
match surface {
|
|
||||||
PreviewSurface::Inline => self
|
|
||||||
.inline_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.map(|feed| feed.install_on_picture(picture, status_label)),
|
|
||||||
PreviewSurface::Window => self
|
|
||||||
.window_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.map(|feed| feed.install_on_picture(picture, status_label)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot_metrics(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
surface: PreviewSurface,
|
|
||||||
) -> Option<PreviewMetricsSnapshot> {
|
|
||||||
match surface {
|
|
||||||
PreviewSurface::Inline => self
|
|
||||||
.inline_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.map(|feed| feed.snapshot_metrics()),
|
|
||||||
PreviewSurface::Window => self
|
|
||||||
.window_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.map(|feed| feed.snapshot_metrics()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_recording_tap(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
surface: PreviewSurface,
|
|
||||||
) -> Option<PreviewRecordingTap> {
|
|
||||||
match surface {
|
|
||||||
PreviewSurface::Inline => self
|
|
||||||
.inline_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.and_then(|feed| feed.start_recording_tap()),
|
|
||||||
PreviewSurface::Window => self
|
|
||||||
.window_feeds
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|feeds| feeds.get(monitor_id).cloned())
|
|
||||||
.and_then(|feed| feed.start_recording_tap()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_capture_profile(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
source_monitor_id: usize,
|
|
||||||
requested_width: i32,
|
|
||||||
requested_height: i32,
|
|
||||||
requested_fps: u32,
|
|
||||||
max_bitrate_kbit: u32,
|
|
||||||
) {
|
|
||||||
let (
|
|
||||||
inline_requested_width,
|
|
||||||
inline_requested_height,
|
|
||||||
inline_requested_fps,
|
|
||||||
inline_max_bitrate_kbit,
|
|
||||||
) = sanitize_preview_request(
|
|
||||||
requested_width,
|
|
||||||
requested_height,
|
|
||||||
requested_fps,
|
|
||||||
max_bitrate_kbit,
|
|
||||||
);
|
|
||||||
self.rebuild_feed(
|
|
||||||
&self.inline_feeds,
|
|
||||||
monitor_id,
|
|
||||||
Some((
|
|
||||||
source_monitor_id,
|
|
||||||
inline_requested_width,
|
|
||||||
inline_requested_height,
|
|
||||||
inline_requested_fps,
|
|
||||||
inline_max_bitrate_kbit,
|
|
||||||
)),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
self.rebuild_feed(
|
|
||||||
&self.window_feeds,
|
|
||||||
monitor_id,
|
|
||||||
Some((
|
|
||||||
source_monitor_id,
|
|
||||||
requested_width,
|
|
||||||
requested_height,
|
|
||||||
requested_fps,
|
|
||||||
max_bitrate_kbit,
|
|
||||||
)),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_breakout_profile(&self, monitor_id: usize, width: i32, height: i32) {
|
|
||||||
self.rebuild_feed(&self.window_feeds, monitor_id, None, Some((width, height)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn profile_for_test(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
surface: PreviewSurface,
|
|
||||||
) -> Option<(u32, i32, i32, i32, i32, u32, u32)> {
|
|
||||||
let feed = match surface {
|
|
||||||
PreviewSurface::Inline => self.inline_feeds.lock().ok()?.get(monitor_id).cloned(),
|
|
||||||
PreviewSurface::Window => self.window_feeds.lock().ok()?.get(monitor_id).cloned(),
|
|
||||||
}?;
|
|
||||||
let profile = feed.profile();
|
|
||||||
Some((
|
|
||||||
profile.source_monitor_id,
|
|
||||||
profile.display_width,
|
|
||||||
profile.display_height,
|
|
||||||
profile.requested_width,
|
|
||||||
profile.requested_height,
|
|
||||||
profile.requested_fps,
|
|
||||||
profile.max_bitrate_kbit,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn feed_disabled_for_test(
|
|
||||||
&self,
|
|
||||||
monitor_id: usize,
|
|
||||||
surface: PreviewSurface,
|
|
||||||
) -> Option<bool> {
|
|
||||||
let feed = match surface {
|
|
||||||
PreviewSurface::Inline => self.inline_feeds.lock().ok()?.get(monitor_id).cloned(),
|
|
||||||
PreviewSurface::Window => self.window_feeds.lock().ok()?.get(monitor_id).cloned(),
|
|
||||||
}?;
|
|
||||||
Some(feed.is_disabled())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rebuild_feed(
|
|
||||||
&self,
|
|
||||||
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
|
|
||||||
monitor_id: usize,
|
|
||||||
requested: Option<(usize, i32, i32, u32, u32)>,
|
|
||||||
display: Option<(i32, i32)>,
|
|
||||||
) {
|
|
||||||
let Ok(mut feeds) = feeds.lock() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(existing) = feeds.get(monitor_id).cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let was_active = existing.is_active();
|
|
||||||
let keep_disabled = existing.is_disabled();
|
|
||||||
let mut profile = existing.profile();
|
|
||||||
if let Some((
|
|
||||||
source_monitor_id,
|
|
||||||
requested_width,
|
|
||||||
requested_height,
|
|
||||||
requested_fps,
|
|
||||||
max_bitrate_kbit,
|
|
||||||
)) = requested
|
|
||||||
{
|
|
||||||
profile.source_monitor_id = source_monitor_id as u32;
|
|
||||||
profile.requested_width = requested_width.max(2);
|
|
||||||
profile.requested_height = requested_height.max(2);
|
|
||||||
profile.requested_fps = requested_fps.max(1);
|
|
||||||
profile.max_bitrate_kbit = max_bitrate_kbit.max(800);
|
|
||||||
}
|
|
||||||
if let Some((display_width, display_height)) = display {
|
|
||||||
profile.display_width = display_width.max(2);
|
|
||||||
profile.display_height = display_height.max(2);
|
|
||||||
}
|
|
||||||
let next_feed = if keep_disabled {
|
|
||||||
Some(PreviewFeed::spawn_disabled(profile))
|
|
||||||
} else {
|
|
||||||
match PreviewFeed::spawn(
|
|
||||||
Arc::clone(&self.server_addr),
|
|
||||||
monitor_id as u32,
|
|
||||||
profile,
|
|
||||||
Arc::clone(&self.log_sink),
|
|
||||||
) {
|
|
||||||
Ok(feed) => Some(feed),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(monitor_id, ?err, "could not rebuild preview feed");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(feed) = next_feed {
|
|
||||||
if was_active {
|
|
||||||
feed.set_active(true);
|
|
||||||
}
|
|
||||||
existing.shutdown();
|
|
||||||
feeds[monitor_id] = feed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_monitor_enabled(&self, monitor_id: usize, enabled: bool) {
|
|
||||||
self.set_feed_enabled(&self.inline_feeds, monitor_id, enabled);
|
|
||||||
self.set_feed_enabled(&self.window_feeds, monitor_id, enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_feed_enabled(
|
|
||||||
&self,
|
|
||||||
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
|
|
||||||
monitor_id: usize,
|
|
||||||
enabled: bool,
|
|
||||||
) {
|
|
||||||
let Ok(mut feeds) = feeds.lock() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(existing) = feeds.get(monitor_id).cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if existing.is_disabled() != enabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let was_active = existing.is_active();
|
|
||||||
let profile = existing.profile();
|
|
||||||
let replacement = if enabled {
|
|
||||||
match PreviewFeed::spawn(
|
|
||||||
Arc::clone(&self.server_addr),
|
|
||||||
monitor_id as u32,
|
|
||||||
profile,
|
|
||||||
Arc::clone(&self.log_sink),
|
|
||||||
) {
|
|
||||||
Ok(feed) => feed,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(monitor_id, ?err, "could not enable preview feed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PreviewFeed::spawn_disabled(profile)
|
|
||||||
};
|
|
||||||
if was_active {
|
|
||||||
replacement.set_active(true);
|
|
||||||
}
|
|
||||||
existing.shutdown();
|
|
||||||
feeds[monitor_id] = replacement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -69,6 +69,10 @@ include!("ui/activation_context.rs");
|
|||||||
include!("ui/startup_window_guard.rs");
|
include!("ui/startup_window_guard.rs");
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
include!("ui/eye_capture_bindings/recording_support.rs");
|
include!("ui/eye_capture_bindings/recording_support.rs");
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
include!("ui/eye_capture_bindings/recording_worker.rs");
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
include!("ui/utility_button_bindings/pki_support.rs");
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
include!("ui/session_preview_coverage.rs");
|
include!("ui/session_preview_coverage.rs");
|
||||||
|
|
||||||
|
|||||||
@ -1,228 +1,4 @@
|
|||||||
{
|
{
|
||||||
fn spawn_raw_video_encoder(
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
output_path: &Path,
|
|
||||||
encode_fps: u32,
|
|
||||||
encode_bitrate_kbit: u32,
|
|
||||||
) -> Result<std::process::Child, String> {
|
|
||||||
let bitrate_arg = format!("{}k", encode_bitrate_kbit.max(800));
|
|
||||||
let fps_arg = encode_fps.max(1).to_string();
|
|
||||||
let video_size = format!("{}x{}", width.max(1), height.max(1));
|
|
||||||
let output_arg = output_path.to_string_lossy().into_owned();
|
|
||||||
Command::new("ffmpeg")
|
|
||||||
.args([
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-y",
|
|
||||||
"-f",
|
|
||||||
"rawvideo",
|
|
||||||
"-pix_fmt",
|
|
||||||
"rgba",
|
|
||||||
"-video_size",
|
|
||||||
&video_size,
|
|
||||||
"-framerate",
|
|
||||||
&fps_arg,
|
|
||||||
"-i",
|
|
||||||
"-",
|
|
||||||
"-c:v",
|
|
||||||
"libx264",
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuv420p",
|
|
||||||
"-r",
|
|
||||||
&fps_arg,
|
|
||||||
"-b:v",
|
|
||||||
&bitrate_arg,
|
|
||||||
])
|
|
||||||
.arg(&output_arg)
|
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.stdout(std::process::Stdio::null())
|
|
||||||
.stderr(std::process::Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.map_err(|err| format!("ffmpeg video encoder is unavailable: {err}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_recording_frame(frame: PreviewFrameSnapshot) -> Result<(i32, i32, Vec<u8>), String> {
|
|
||||||
let width = frame.width.max(0) as usize;
|
|
||||||
let height = frame.height.max(0) as usize;
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
return Err("decoded preview frame had zero size".to_string());
|
|
||||||
}
|
|
||||||
let row_bytes = width.saturating_mul(4);
|
|
||||||
let needed = row_bytes.saturating_mul(height);
|
|
||||||
if frame.rgba.len() < needed && frame.stride == row_bytes {
|
|
||||||
return Err("decoded preview frame was shorter than its declared size".to_string());
|
|
||||||
}
|
|
||||||
if frame.stride == row_bytes && frame.rgba.len() >= needed {
|
|
||||||
return Ok((frame.width, frame.height, frame.rgba[..needed].to_vec()));
|
|
||||||
}
|
|
||||||
if frame.stride < row_bytes || frame.rgba.len() < frame.stride.saturating_mul(height) {
|
|
||||||
return Err("decoded preview frame stride was inconsistent".to_string());
|
|
||||||
}
|
|
||||||
let mut rgba = Vec::with_capacity(needed);
|
|
||||||
for row in 0..height {
|
|
||||||
let start = row.saturating_mul(frame.stride);
|
|
||||||
rgba.extend_from_slice(&frame.rgba[start..start + row_bytes]);
|
|
||||||
}
|
|
||||||
Ok((frame.width, frame.height, rgba))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish_raw_video_encoder(
|
|
||||||
child: &mut std::process::Child,
|
|
||||||
frame_dir: &Path,
|
|
||||||
output_path: &Path,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let _ = child.stdin.take();
|
|
||||||
let status = child
|
|
||||||
.wait()
|
|
||||||
.map_err(|err| format!("ffmpeg video encoder wait failed: {err}"))?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(format!(
|
|
||||||
"ffmpeg failed while encoding {}; temporary data is still in {}",
|
|
||||||
output_path.display(),
|
|
||||||
frame_dir.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mux_recording_audio(
|
|
||||||
video_path: &Path,
|
|
||||||
output_path: &Path,
|
|
||||||
audio_mode: EyeRecordAudioMode,
|
|
||||||
audio_paths: &[PathBuf],
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let usable_audio_paths = validated_audio_paths(audio_mode, audio_paths)?;
|
|
||||||
if usable_audio_paths.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let video_arg = video_path.to_string_lossy().into_owned();
|
|
||||||
let output_arg = output_path.to_string_lossy().into_owned();
|
|
||||||
let mut command = Command::new("ffmpeg");
|
|
||||||
command.args([
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-y",
|
|
||||||
"-i",
|
|
||||||
&video_arg,
|
|
||||||
]);
|
|
||||||
for audio_path in &usable_audio_paths {
|
|
||||||
command.arg("-i").arg(audio_path);
|
|
||||||
}
|
|
||||||
if usable_audio_paths.len() > 1 {
|
|
||||||
command.args([
|
|
||||||
"-filter_complex",
|
|
||||||
"[1:a][2:a]amix=inputs=2:duration=shortest:normalize=0[a]",
|
|
||||||
"-map",
|
|
||||||
"0:v:0",
|
|
||||||
"-map",
|
|
||||||
"[a]",
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
command.args(["-map", "0:v:0", "-map", "1:a:0"]);
|
|
||||||
}
|
|
||||||
command.args(["-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest"]);
|
|
||||||
let mux = command
|
|
||||||
.arg(&output_arg)
|
|
||||||
.status()
|
|
||||||
.map_err(|err| format!("ffmpeg is unavailable: {err}"))?;
|
|
||||||
if !mux.success() {
|
|
||||||
return Err(format!(
|
|
||||||
"ffmpeg failed while adding audio to {}",
|
|
||||||
output_path.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_recording_worker(
|
|
||||||
frame_tap: PreviewRecordingTap,
|
|
||||||
control_rx: std::sync::mpsc::Receiver<RecordFrameTask>,
|
|
||||||
frame_dir: PathBuf,
|
|
||||||
output_path: PathBuf,
|
|
||||||
encode_fps: u32,
|
|
||||||
encode_bitrate_kbit: u32,
|
|
||||||
audio_mode: EyeRecordAudioMode,
|
|
||||||
audio_paths: Vec<PathBuf>,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
let needs_audio_mux = audio_mode != EyeRecordAudioMode::NoAudio;
|
|
||||||
let video_output_path = if needs_audio_mux {
|
|
||||||
frame_dir.join("recording-video.mp4")
|
|
||||||
} else {
|
|
||||||
output_path.clone()
|
|
||||||
};
|
|
||||||
let mut encoder: Option<std::process::Child> = None;
|
|
||||||
let mut frame_size: Option<(i32, i32)> = None;
|
|
||||||
let mut captured_frames = 0_u32;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match control_rx.try_recv() {
|
|
||||||
Ok(RecordFrameTask::Finish) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(std::sync::mpsc::TryRecvError::Empty) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match frame_tap.recv_timeout(Duration::from_millis(50)) {
|
|
||||||
Ok(frame) => {
|
|
||||||
let (width, height, rgba) = normalize_recording_frame(frame)?;
|
|
||||||
if let Some((expected_width, expected_height)) = frame_size {
|
|
||||||
if width != expected_width || height != expected_height {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
frame_size = Some((width, height));
|
|
||||||
encoder = Some(spawn_raw_video_encoder(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
&video_output_path,
|
|
||||||
encode_fps,
|
|
||||||
encode_bitrate_kbit,
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
let encoder = encoder
|
|
||||||
.as_mut()
|
|
||||||
.ok_or_else(|| "recording encoder did not start".to_string())?;
|
|
||||||
let stdin = encoder
|
|
||||||
.stdin
|
|
||||||
.as_mut()
|
|
||||||
.ok_or_else(|| "recording encoder stdin is closed".to_string())?;
|
|
||||||
std::io::Write::write_all(stdin, &rgba)
|
|
||||||
.map_err(|err| format!("recording encoder write failed: {err}"))?;
|
|
||||||
captured_frames = captured_frames.saturating_add(1);
|
|
||||||
}
|
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
|
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if captured_frames < 2 {
|
|
||||||
if let Some(mut child) = encoder {
|
|
||||||
let _ = child.kill();
|
|
||||||
let _ = child.wait();
|
|
||||||
}
|
|
||||||
let _ = std::fs::remove_dir_all(&frame_dir);
|
|
||||||
return Err("need at least two captured frames to build a recording".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mut child) = encoder {
|
|
||||||
finish_raw_video_encoder(&mut child, &frame_dir, &video_output_path)?;
|
|
||||||
}
|
|
||||||
mux_recording_audio(
|
|
||||||
&video_output_path,
|
|
||||||
&output_path,
|
|
||||||
audio_mode,
|
|
||||||
&audio_paths,
|
|
||||||
)?;
|
|
||||||
if needs_audio_mux {
|
|
||||||
let _ = std::fs::remove_file(&video_output_path);
|
|
||||||
}
|
|
||||||
let _ = std::fs::remove_dir_all(&frame_dir);
|
|
||||||
Ok(output_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
for monitor_id in 0..2 {
|
for monitor_id in 0..2 {
|
||||||
let pane = widgets.display_panes[monitor_id].clone();
|
let pane = widgets.display_panes[monitor_id].clone();
|
||||||
let widgets_for_ui = widgets.clone();
|
let widgets_for_ui = widgets.clone();
|
||||||
@ -477,15 +253,17 @@
|
|||||||
let frame_dir_worker = frame_dir.clone();
|
let frame_dir_worker = frame_dir.clone();
|
||||||
let output_path_worker = output_path.clone();
|
let output_path_worker = output_path.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = run_recording_worker(
|
let result = run_recording_worker(
|
||||||
frame_tap,
|
frame_tap,
|
||||||
control_rx,
|
control_rx,
|
||||||
frame_dir_worker,
|
EyeRecordingWorkerConfig {
|
||||||
output_path_worker,
|
frame_dir: frame_dir_worker,
|
||||||
record_fps,
|
output_path: output_path_worker,
|
||||||
record_bitrate_kbit,
|
encode_fps: record_fps,
|
||||||
audio_mode,
|
encode_bitrate_kbit: record_bitrate_kbit,
|
||||||
audio_paths,
|
audio_mode,
|
||||||
|
audio_paths,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let _ = result_tx.send(result);
|
let _ = result_tx.send(result);
|
||||||
});
|
});
|
||||||
|
|||||||
235
client/src/launcher/ui/eye_capture_bindings/recording_worker.rs
Normal file
235
client/src/launcher/ui/eye_capture_bindings/recording_worker.rs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
fn spawn_raw_video_encoder(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
output_path: &Path,
|
||||||
|
encode_fps: u32,
|
||||||
|
encode_bitrate_kbit: u32,
|
||||||
|
) -> Result<std::process::Child, String> {
|
||||||
|
let bitrate_arg = format!("{}k", encode_bitrate_kbit.max(800));
|
||||||
|
let fps_arg = encode_fps.max(1).to_string();
|
||||||
|
let video_size = format!("{}x{}", width.max(1), height.max(1));
|
||||||
|
let output_arg = output_path.to_string_lossy().into_owned();
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args([
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"-pix_fmt",
|
||||||
|
"rgba",
|
||||||
|
"-video_size",
|
||||||
|
&video_size,
|
||||||
|
"-framerate",
|
||||||
|
&fps_arg,
|
||||||
|
"-i",
|
||||||
|
"-",
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
"-r",
|
||||||
|
&fps_arg,
|
||||||
|
"-b:v",
|
||||||
|
&bitrate_arg,
|
||||||
|
])
|
||||||
|
.arg(&output_arg)
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|err| format!("ffmpeg video encoder is unavailable: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_recording_frame(frame: PreviewFrameSnapshot) -> Result<(i32, i32, Vec<u8>), String> {
|
||||||
|
let width = frame.width.max(0) as usize;
|
||||||
|
let height = frame.height.max(0) as usize;
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err("decoded preview frame had zero size".to_string());
|
||||||
|
}
|
||||||
|
let row_bytes = width.saturating_mul(4);
|
||||||
|
let needed = row_bytes.saturating_mul(height);
|
||||||
|
if frame.rgba.len() < needed && frame.stride == row_bytes {
|
||||||
|
return Err("decoded preview frame was shorter than its declared size".to_string());
|
||||||
|
}
|
||||||
|
if frame.stride == row_bytes && frame.rgba.len() >= needed {
|
||||||
|
return Ok((frame.width, frame.height, frame.rgba[..needed].to_vec()));
|
||||||
|
}
|
||||||
|
if frame.stride < row_bytes || frame.rgba.len() < frame.stride.saturating_mul(height) {
|
||||||
|
return Err("decoded preview frame stride was inconsistent".to_string());
|
||||||
|
}
|
||||||
|
let mut rgba = Vec::with_capacity(needed);
|
||||||
|
for row in 0..height {
|
||||||
|
let start = row.saturating_mul(frame.stride);
|
||||||
|
rgba.extend_from_slice(&frame.rgba[start..start + row_bytes]);
|
||||||
|
}
|
||||||
|
Ok((frame.width, frame.height, rgba))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_raw_video_encoder(
|
||||||
|
child: &mut std::process::Child,
|
||||||
|
frame_dir: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let _ = child.stdin.take();
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.map_err(|err| format!("ffmpeg video encoder wait failed: {err}"))?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"ffmpeg failed while encoding {}; temporary data is still in {}",
|
||||||
|
output_path.display(),
|
||||||
|
frame_dir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mux_recording_audio(
|
||||||
|
video_path: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
audio_mode: EyeRecordAudioMode,
|
||||||
|
audio_paths: &[PathBuf],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let usable_audio_paths = validated_audio_paths(audio_mode, audio_paths)?;
|
||||||
|
if usable_audio_paths.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let video_arg = video_path.to_string_lossy().into_owned();
|
||||||
|
let output_arg = output_path.to_string_lossy().into_owned();
|
||||||
|
let mut command = Command::new("ffmpeg");
|
||||||
|
command.args([
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
&video_arg,
|
||||||
|
]);
|
||||||
|
for audio_path in &usable_audio_paths {
|
||||||
|
command.arg("-i").arg(audio_path);
|
||||||
|
}
|
||||||
|
if usable_audio_paths.len() > 1 {
|
||||||
|
command.args([
|
||||||
|
"-filter_complex",
|
||||||
|
"[1:a][2:a]amix=inputs=2:duration=shortest:normalize=0[a]",
|
||||||
|
"-map",
|
||||||
|
"0:v:0",
|
||||||
|
"-map",
|
||||||
|
"[a]",
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
command.args(["-map", "0:v:0", "-map", "1:a:0"]);
|
||||||
|
}
|
||||||
|
command.args(["-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest"]);
|
||||||
|
let mux = command
|
||||||
|
.arg(&output_arg)
|
||||||
|
.status()
|
||||||
|
.map_err(|err| format!("ffmpeg is unavailable: {err}"))?;
|
||||||
|
if !mux.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"ffmpeg failed while adding audio to {}",
|
||||||
|
output_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EyeRecordingWorkerConfig {
|
||||||
|
frame_dir: PathBuf,
|
||||||
|
output_path: PathBuf,
|
||||||
|
encode_fps: u32,
|
||||||
|
encode_bitrate_kbit: u32,
|
||||||
|
audio_mode: EyeRecordAudioMode,
|
||||||
|
audio_paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_recording_worker(
|
||||||
|
frame_tap: PreviewRecordingTap,
|
||||||
|
control_rx: std::sync::mpsc::Receiver<RecordFrameTask>,
|
||||||
|
config: EyeRecordingWorkerConfig,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let EyeRecordingWorkerConfig {
|
||||||
|
frame_dir,
|
||||||
|
output_path,
|
||||||
|
encode_fps,
|
||||||
|
encode_bitrate_kbit,
|
||||||
|
audio_mode,
|
||||||
|
audio_paths,
|
||||||
|
} = config;
|
||||||
|
let needs_audio_mux = audio_mode != EyeRecordAudioMode::NoAudio;
|
||||||
|
let video_output_path = if needs_audio_mux {
|
||||||
|
frame_dir.join("recording-video.mp4")
|
||||||
|
} else {
|
||||||
|
output_path.clone()
|
||||||
|
};
|
||||||
|
let mut encoder: Option<std::process::Child> = None;
|
||||||
|
let mut frame_size: Option<(i32, i32)> = None;
|
||||||
|
let mut captured_frames = 0_u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match control_rx.try_recv() {
|
||||||
|
Ok(RecordFrameTask::Finish) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Empty) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match frame_tap.recv_timeout(Duration::from_millis(50)) {
|
||||||
|
Ok(frame) => {
|
||||||
|
let (width, height, rgba) = normalize_recording_frame(frame)?;
|
||||||
|
if let Some((expected_width, expected_height)) = frame_size {
|
||||||
|
if width != expected_width || height != expected_height {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame_size = Some((width, height));
|
||||||
|
encoder = Some(spawn_raw_video_encoder(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
&video_output_path,
|
||||||
|
encode_fps,
|
||||||
|
encode_bitrate_kbit,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
let encoder = encoder
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| "recording encoder did not start".to_string())?;
|
||||||
|
let stdin = encoder
|
||||||
|
.stdin
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| "recording encoder stdin is closed".to_string())?;
|
||||||
|
std::io::Write::write_all(stdin, &rgba)
|
||||||
|
.map_err(|err| format!("recording encoder write failed: {err}"))?;
|
||||||
|
captured_frames = captured_frames.saturating_add(1);
|
||||||
|
}
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if captured_frames < 2 {
|
||||||
|
if let Some(mut child) = encoder {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&frame_dir);
|
||||||
|
return Err("need at least two captured frames to build a recording".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut child) = encoder {
|
||||||
|
finish_raw_video_encoder(&mut child, &frame_dir, &video_output_path)?;
|
||||||
|
}
|
||||||
|
mux_recording_audio(
|
||||||
|
&video_output_path,
|
||||||
|
&output_path,
|
||||||
|
audio_mode,
|
||||||
|
&audio_paths,
|
||||||
|
)?;
|
||||||
|
if needs_audio_mux {
|
||||||
|
let _ = std::fs::remove_file(&video_output_path);
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&frame_dir);
|
||||||
|
Ok(output_path)
|
||||||
|
}
|
||||||
@ -1,64 +1,4 @@
|
|||||||
{
|
{
|
||||||
fn default_client_pki_dir() -> PathBuf {
|
|
||||||
if let Some(home) = std::env::var_os("HOME") {
|
|
||||||
return PathBuf::from(home).join(".config/lesavka/pki");
|
|
||||||
}
|
|
||||||
std::env::current_dir()
|
|
||||||
.unwrap_or_else(|_| PathBuf::from("."))
|
|
||||||
.join(".config/lesavka/pki")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_client_pki_bundle(bundle: &Path) -> Result<PathBuf, String> {
|
|
||||||
let target = default_client_pki_dir();
|
|
||||||
let scratch = std::env::temp_dir().join(format!(
|
|
||||||
"lesavka-client-pki-{}",
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_millis()
|
|
||||||
));
|
|
||||||
std::fs::create_dir_all(&scratch)
|
|
||||||
.map_err(|err| format!("could not create extraction folder: {err}"))?;
|
|
||||||
let extract = Command::new("tar")
|
|
||||||
.arg("-xzf")
|
|
||||||
.arg(bundle)
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&scratch)
|
|
||||||
.status()
|
|
||||||
.map_err(|err| format!("tar is unavailable: {err}"))?;
|
|
||||||
if !extract.success() {
|
|
||||||
let _ = std::fs::remove_dir_all(&scratch);
|
|
||||||
return Err(format!("could not extract {}", bundle.display()));
|
|
||||||
}
|
|
||||||
for item in ["ca.crt", "client.crt", "client.key"] {
|
|
||||||
if !scratch.join(item).is_file() {
|
|
||||||
let _ = std::fs::remove_dir_all(&scratch);
|
|
||||||
return Err(format!("bundle is missing {item}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::fs::create_dir_all(&target)
|
|
||||||
.map_err(|err| format!("could not create {}: {err}", target.display()))?;
|
|
||||||
for item in ["ca.crt", "client.crt", "client.key"] {
|
|
||||||
std::fs::copy(scratch.join(item), target.join(item))
|
|
||||||
.map_err(|err| format!("could not install {item}: {err}"))?;
|
|
||||||
}
|
|
||||||
tighten_client_key_permissions(&target.join("client.key"));
|
|
||||||
let _ = std::fs::remove_dir_all(&scratch);
|
|
||||||
Ok(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn tighten_client_key_permissions(path: &Path) {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
if let Ok(mut permissions) = std::fs::metadata(path).map(|metadata| metadata.permissions()) {
|
|
||||||
permissions.set_mode(0o600);
|
|
||||||
let _ = std::fs::set_permissions(path, permissions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
fn tighten_client_key_permissions(_path: &Path) {}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
fn default_client_pki_dir() -> PathBuf {
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
return PathBuf::from(home).join(".config/lesavka/pki");
|
||||||
|
}
|
||||||
|
std::env::current_dir()
|
||||||
|
.unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
.join(".config/lesavka/pki")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_client_pki_bundle(bundle: &Path) -> Result<PathBuf, String> {
|
||||||
|
let target = default_client_pki_dir();
|
||||||
|
let scratch = std::env::temp_dir().join(format!(
|
||||||
|
"lesavka-client-pki-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&scratch)
|
||||||
|
.map_err(|err| format!("could not create extraction folder: {err}"))?;
|
||||||
|
let extract = Command::new("tar")
|
||||||
|
.arg("-xzf")
|
||||||
|
.arg(bundle)
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&scratch)
|
||||||
|
.status()
|
||||||
|
.map_err(|err| format!("tar is unavailable: {err}"))?;
|
||||||
|
if !extract.success() {
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
return Err(format!("could not extract {}", bundle.display()));
|
||||||
|
}
|
||||||
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
||||||
|
if !scratch.join(item).is_file() {
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
return Err(format!("bundle is missing {item}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&target)
|
||||||
|
.map_err(|err| format!("could not create {}: {err}", target.display()))?;
|
||||||
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
||||||
|
std::fs::copy(scratch.join(item), target.join(item))
|
||||||
|
.map_err(|err| format!("could not install {item}: {err}"))?;
|
||||||
|
}
|
||||||
|
tighten_client_key_permissions(&target.join("client.key"));
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn tighten_client_key_permissions(path: &Path) {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
if let Ok(mut permissions) = std::fs::metadata(path).map(|metadata| metadata.permissions()) {
|
||||||
|
permissions.set_mode(0o600);
|
||||||
|
let _ = std::fs::set_permissions(path, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn tighten_client_key_permissions(_path: &Path) {}
|
||||||
@ -183,6 +183,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_LAUNCHER_PARENT_PID` | launcher UI/runtime override |
|
| `LESAVKA_LAUNCHER_PARENT_PID` | launcher UI/runtime override |
|
||||||
| `LESAVKA_LAUNCHER_PARENT_START_TICKS` | launcher UI/runtime override |
|
| `LESAVKA_LAUNCHER_PARENT_START_TICKS` | launcher UI/runtime override |
|
||||||
| `LESAVKA_LAUNCHER_TOGGLE_KEY_CONTROL` | launcher UI/runtime override |
|
| `LESAVKA_LAUNCHER_TOGGLE_KEY_CONTROL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_WAKE_CONTROL` | launcher idle-wake control file override; stores staged relay idle-nudge requests from the UI |
|
||||||
| `LESAVKA_LAUNCHER_WINDOW_TITLE` | launcher UI/runtime override |
|
| `LESAVKA_LAUNCHER_WINDOW_TITLE` | launcher UI/runtime override |
|
||||||
| `LESAVKA_LAB_GATE_PUSHGATEWAY_JOB` | CI metrics destination override for the opt-in bare-metal lab gate profile |
|
| `LESAVKA_LAB_GATE_PUSHGATEWAY_JOB` | CI metrics destination override for the opt-in bare-metal lab gate profile |
|
||||||
| `LESAVKA_LIVE_KEYBOARD_REPORT_DELAY_MS` | input routing/clipboard override |
|
| `LESAVKA_LIVE_KEYBOARD_REPORT_DELAY_MS` | input routing/clipboard override |
|
||||||
@ -355,6 +356,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_UVC_MJPEG` | server hardware/device override |
|
| `LESAVKA_UVC_MJPEG` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC` | UVC helper MJPEG budget guard; derives a per-frame byte cap from target FPS when `LESAVKA_UVC_FRAME_MAX_BYTES` is unset; non-bulk UVC is additionally capped by `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT` |
|
| `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC` | UVC helper MJPEG budget guard; derives a per-frame byte cap from target FPS when `LESAVKA_UVC_FRAME_MAX_BYTES` is unset; non-bulk UVC is additionally capped by `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT` |
|
||||||
| `LESAVKA_UVC_QUEUE_PACING` | UVC helper queue pacing override; defaults to `0` because the RCT host already paces UVC consumption, and delaying returned buffer requeueing can starve isochronous gadget transfers |
|
| `LESAVKA_UVC_QUEUE_PACING` | UVC helper queue pacing override; defaults to `0` because the RCT host already paces UVC consumption, and delaying returned buffer requeueing can starve isochronous gadget transfers |
|
||||||
|
| `LESAVKA_UVC_RESTART_DELAY_MS` | UVC control helper supervisor restart delay after helper exit or failure; defaults to `1000` |
|
||||||
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_STATS_INTERVAL_MS` | UVC helper telemetry interval for queued/reloaded/rejected MJPEG frame counters; defaults to `5000`, `0` disables |
|
| `LESAVKA_UVC_STATS_INTERVAL_MS` | UVC helper telemetry interval for queued/reloaded/rejected MJPEG frame counters; defaults to `5000`, `0` disables |
|
||||||
| `LESAVKA_UVC_STATS_PATH` | UVC helper JSON stats snapshot path for queued/reloaded/rejected MJPEG frame counters; defaults to `/run/lesavka-uvc-video-stats.json`, set `0` or empty to disable file snapshots |
|
| `LESAVKA_UVC_STATS_PATH` | UVC helper JSON stats snapshot path for queued/reloaded/rejected MJPEG frame counters; defaults to `/run/lesavka-uvc-video-stats.json`, set `0` or empty to disable file snapshots |
|
||||||
|
|||||||
@ -168,12 +168,12 @@
|
|||||||
"client/src/input/inputs.rs": {
|
"client/src/input/inputs.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 87
|
"loc": 94
|
||||||
},
|
},
|
||||||
"client/src/input/inputs/construction_and_scan.rs": {
|
"client/src/input/inputs/construction_and_scan.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 4,
|
"doc_debt": 4,
|
||||||
"loc": 275
|
"loc": 292
|
||||||
},
|
},
|
||||||
"client/src/input/inputs/device_classification.rs": {
|
"client/src/input/inputs/device_classification.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -182,18 +182,18 @@
|
|||||||
},
|
},
|
||||||
"client/src/input/inputs/routing_state.rs": {
|
"client/src/input/inputs/routing_state.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 11,
|
"doc_debt": 14,
|
||||||
"loc": 293
|
"loc": 379
|
||||||
},
|
},
|
||||||
"client/src/input/inputs/run_loop.rs": {
|
"client/src/input/inputs/run_loop.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 2,
|
"doc_debt": 2,
|
||||||
"loc": 143
|
"loc": 149
|
||||||
},
|
},
|
||||||
"client/src/input/inputs/runtime_controls.rs": {
|
"client/src/input/inputs/runtime_controls.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 4,
|
"doc_debt": 5,
|
||||||
"loc": 127
|
"loc": 145
|
||||||
},
|
},
|
||||||
"client/src/input/inputs/toggle_keys.rs": {
|
"client/src/input/inputs/toggle_keys.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -238,7 +238,7 @@
|
|||||||
"client/src/input/mouse.rs": {
|
"client/src/input/mouse.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 13,
|
"doc_debt": 13,
|
||||||
"loc": 411
|
"loc": 416
|
||||||
},
|
},
|
||||||
"client/src/input/mouse_event_contract_tests.rs": {
|
"client/src/input/mouse_event_contract_tests.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -318,27 +318,37 @@
|
|||||||
"client/src/launcher/preview.rs": {
|
"client/src/launcher/preview.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 10
|
"loc": 12
|
||||||
},
|
},
|
||||||
"client/src/launcher/preview/feed_runtime.rs": {
|
"client/src/launcher/preview/feed_runtime.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 7,
|
"doc_debt": 7,
|
||||||
"loc": 500
|
"loc": 226
|
||||||
},
|
},
|
||||||
"client/src/launcher/preview/feed_state.rs": {
|
"client/src/launcher/preview/feed_state.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 12,
|
"doc_debt": 13,
|
||||||
"loc": 304
|
"loc": 315
|
||||||
|
},
|
||||||
|
"client/src/launcher/preview/feed_worker.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 1,
|
||||||
|
"loc": 312
|
||||||
},
|
},
|
||||||
"client/src/launcher/preview/frame_telemetry.rs": {
|
"client/src/launcher/preview/frame_telemetry.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 9,
|
"doc_debt": 9,
|
||||||
"loc": 179
|
"loc": 179
|
||||||
},
|
},
|
||||||
|
"client/src/launcher/preview/launcher_preview_impl.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 13,
|
||||||
|
"loc": 334
|
||||||
|
},
|
||||||
"client/src/launcher/preview/preview_core.rs": {
|
"client/src/launcher/preview/preview_core.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 14,
|
"doc_debt": 1,
|
||||||
"loc": 500
|
"loc": 200
|
||||||
},
|
},
|
||||||
"client/src/launcher/preview/status_pipeline.rs": {
|
"client/src/launcher/preview/status_pipeline.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -378,7 +388,7 @@
|
|||||||
"client/src/launcher/ui.rs": {
|
"client/src/launcher/ui.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 1,
|
"doc_debt": 1,
|
||||||
"loc": 204
|
"loc": 213
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/activation_context.rs": {
|
"client/src/launcher/ui/activation_context.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -407,13 +417,18 @@
|
|||||||
},
|
},
|
||||||
"client/src/launcher/ui/eye_capture_bindings.rs": {
|
"client/src/launcher/ui/eye_capture_bindings.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 2,
|
"doc_debt": 0,
|
||||||
"loc": 435
|
"loc": 294
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/eye_capture_bindings/recording_support.rs": {
|
"client/src/launcher/ui/eye_capture_bindings/recording_support.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 24,
|
"doc_debt": 23,
|
||||||
"loc": 449
|
"loc": 421
|
||||||
|
},
|
||||||
|
"client/src/launcher/ui/eye_capture_bindings/recording_worker.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 5,
|
||||||
|
"loc": 235
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/eye_display_bindings.rs": {
|
"client/src/launcher/ui/eye_display_bindings.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -476,24 +491,29 @@
|
|||||||
"loc": 53
|
"loc": 53
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/utility_button_bindings.rs": {
|
"client/src/launcher/ui/utility_button_bindings.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 0,
|
||||||
|
"loc": 481
|
||||||
|
},
|
||||||
|
"client/src/launcher/ui/utility_button_bindings/pki_support.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 3,
|
"doc_debt": 3,
|
||||||
"loc": 489
|
"loc": 59
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components.rs": {
|
"client/src/launcher/ui_components.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 1,
|
"doc_debt": 1,
|
||||||
"loc": 136
|
"loc": 137
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/assemble_view.rs": {
|
"client/src/launcher/ui_components/assemble_view.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 216
|
"loc": 217
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_contexts.rs": {
|
"client/src/launcher/ui_components/build_contexts.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 96
|
"loc": 97
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_device_controls.rs": {
|
"client/src/launcher/ui_components/build_device_controls.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -503,7 +523,7 @@
|
|||||||
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 332
|
"loc": 352
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_shell.rs": {
|
"client/src/launcher/ui_components/build_shell.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -523,7 +543,7 @@
|
|||||||
"client/src/launcher/ui_components/display_pane.rs": {
|
"client/src/launcher/ui_components/display_pane.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 2,
|
"doc_debt": 2,
|
||||||
"loc": 247
|
"loc": 249
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/panel_chips.rs": {
|
"client/src/launcher/ui_components/panel_chips.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -543,7 +563,7 @@
|
|||||||
"client/src/launcher/ui_components/types.rs": {
|
"client/src/launcher/ui_components/types.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 237
|
"loc": 238
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime.rs": {
|
"client/src/launcher/ui_runtime.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -553,12 +573,12 @@
|
|||||||
"client/src/launcher/ui_runtime/control_paths.rs": {
|
"client/src/launcher/ui_runtime/control_paths.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 302
|
"loc": 316
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 5,
|
"doc_debt": 5,
|
||||||
"loc": 273
|
"loc": 277
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime/log_filtering.rs": {
|
"client/src/launcher/ui_runtime/log_filtering.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -568,7 +588,7 @@
|
|||||||
"client/src/launcher/ui_runtime/process_logs.rs": {
|
"client/src/launcher/ui_runtime/process_logs.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 7,
|
"doc_debt": 7,
|
||||||
"loc": 277
|
"loc": 279
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime/report_popouts.rs": {
|
"client/src/launcher/ui_runtime/report_popouts.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -588,7 +608,7 @@
|
|||||||
"client/src/launcher/ui_runtime/status_refresh.rs": {
|
"client/src/launcher/ui_runtime/status_refresh.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 3,
|
"doc_debt": 3,
|
||||||
"loc": 444
|
"loc": 446
|
||||||
},
|
},
|
||||||
"client/src/layout.rs": {
|
"client/src/layout.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -918,7 +938,7 @@
|
|||||||
"server/src/audio/ear_capture.rs": {
|
"server/src/audio/ear_capture.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 9,
|
"doc_debt": 9,
|
||||||
"loc": 450
|
"loc": 451
|
||||||
},
|
},
|
||||||
"server/src/audio/ear_capture/source_watchdog.rs": {
|
"server/src/audio/ear_capture/source_watchdog.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -1253,7 +1273,7 @@
|
|||||||
"server/src/uvc_runtime.rs": {
|
"server/src/uvc_runtime.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 4,
|
"doc_debt": 4,
|
||||||
"loc": 255
|
"loc": 264
|
||||||
},
|
},
|
||||||
"server/src/video.rs": {
|
"server/src/video.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
|
|||||||
@ -19,10 +19,10 @@ use tokio::runtime::Runtime;
|
|||||||
async fn wait_for_marker(marker: &Path, is_ready: impl Fn(&str) -> bool) -> String {
|
async fn wait_for_marker(marker: &Path, is_ready: impl Fn(&str) -> bool) -> String {
|
||||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
|
||||||
loop {
|
loop {
|
||||||
if let Ok(contents) = fs::read_to_string(marker) {
|
if let Ok(contents) = fs::read_to_string(marker)
|
||||||
if is_ready(&contents) {
|
&& is_ready(&contents)
|
||||||
return contents;
|
{
|
||||||
}
|
return contents;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
tokio::time::Instant::now() < deadline,
|
tokio::time::Instant::now() < deadline,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user