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/status_pipeline.rs");
|
||||
include!("preview/frame_telemetry.rs");
|
||||
include!("preview/feed_worker.rs");
|
||||
include!("preview/launcher_preview_impl.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
#[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");
|
||||
#[cfg(not(coverage))]
|
||||
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)]
|
||||
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 {
|
||||
let pane = widgets.display_panes[monitor_id].clone();
|
||||
let widgets_for_ui = widgets.clone();
|
||||
@ -477,15 +253,17 @@
|
||||
let frame_dir_worker = frame_dir.clone();
|
||||
let output_path_worker = output_path.clone();
|
||||
std::thread::spawn(move || {
|
||||
let result = run_recording_worker(
|
||||
let result = run_recording_worker(
|
||||
frame_tap,
|
||||
control_rx,
|
||||
frame_dir_worker,
|
||||
output_path_worker,
|
||||
record_fps,
|
||||
record_bitrate_kbit,
|
||||
audio_mode,
|
||||
audio_paths,
|
||||
EyeRecordingWorkerConfig {
|
||||
frame_dir: frame_dir_worker,
|
||||
output_path: output_path_worker,
|
||||
encode_fps: record_fps,
|
||||
encode_bitrate_kbit: record_bitrate_kbit,
|
||||
audio_mode,
|
||||
audio_paths,
|
||||
},
|
||||
);
|
||||
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 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_START_TICKS` | 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_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 |
|
||||
@ -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_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_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_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 |
|
||||
|
||||
@ -168,12 +168,12 @@
|
||||
"client/src/input/inputs.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 87
|
||||
"loc": 94
|
||||
},
|
||||
"client/src/input/inputs/construction_and_scan.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 4,
|
||||
"loc": 275
|
||||
"loc": 292
|
||||
},
|
||||
"client/src/input/inputs/device_classification.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -182,18 +182,18 @@
|
||||
},
|
||||
"client/src/input/inputs/routing_state.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 11,
|
||||
"loc": 293
|
||||
"doc_debt": 14,
|
||||
"loc": 379
|
||||
},
|
||||
"client/src/input/inputs/run_loop.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 2,
|
||||
"loc": 143
|
||||
"loc": 149
|
||||
},
|
||||
"client/src/input/inputs/runtime_controls.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 4,
|
||||
"loc": 127
|
||||
"doc_debt": 5,
|
||||
"loc": 145
|
||||
},
|
||||
"client/src/input/inputs/toggle_keys.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -238,7 +238,7 @@
|
||||
"client/src/input/mouse.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 13,
|
||||
"loc": 411
|
||||
"loc": 416
|
||||
},
|
||||
"client/src/input/mouse_event_contract_tests.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -318,27 +318,37 @@
|
||||
"client/src/launcher/preview.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 10
|
||||
"loc": 12
|
||||
},
|
||||
"client/src/launcher/preview/feed_runtime.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 7,
|
||||
"loc": 500
|
||||
"loc": 226
|
||||
},
|
||||
"client/src/launcher/preview/feed_state.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 12,
|
||||
"loc": 304
|
||||
"doc_debt": 13,
|
||||
"loc": 315
|
||||
},
|
||||
"client/src/launcher/preview/feed_worker.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 1,
|
||||
"loc": 312
|
||||
},
|
||||
"client/src/launcher/preview/frame_telemetry.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 9,
|
||||
"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": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 14,
|
||||
"loc": 500
|
||||
"doc_debt": 1,
|
||||
"loc": 200
|
||||
},
|
||||
"client/src/launcher/preview/status_pipeline.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -378,7 +388,7 @@
|
||||
"client/src/launcher/ui.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 1,
|
||||
"loc": 204
|
||||
"loc": 213
|
||||
},
|
||||
"client/src/launcher/ui/activation_context.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -407,13 +417,18 @@
|
||||
},
|
||||
"client/src/launcher/ui/eye_capture_bindings.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 2,
|
||||
"loc": 435
|
||||
"doc_debt": 0,
|
||||
"loc": 294
|
||||
},
|
||||
"client/src/launcher/ui/eye_capture_bindings/recording_support.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 24,
|
||||
"loc": 449
|
||||
"doc_debt": 23,
|
||||
"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": {
|
||||
"clippy_warnings": 0,
|
||||
@ -476,24 +491,29 @@
|
||||
"loc": 53
|
||||
},
|
||||
"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,
|
||||
"doc_debt": 3,
|
||||
"loc": 489
|
||||
"loc": 59
|
||||
},
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 1,
|
||||
"loc": 136
|
||||
"loc": 137
|
||||
},
|
||||
"client/src/launcher/ui_components/assemble_view.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 216
|
||||
"loc": 217
|
||||
},
|
||||
"client/src/launcher/ui_components/build_contexts.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 96
|
||||
"loc": 97
|
||||
},
|
||||
"client/src/launcher/ui_components/build_device_controls.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -503,7 +523,7 @@
|
||||
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 332
|
||||
"loc": 352
|
||||
},
|
||||
"client/src/launcher/ui_components/build_shell.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -523,7 +543,7 @@
|
||||
"client/src/launcher/ui_components/display_pane.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 2,
|
||||
"loc": 247
|
||||
"loc": 249
|
||||
},
|
||||
"client/src/launcher/ui_components/panel_chips.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -543,7 +563,7 @@
|
||||
"client/src/launcher/ui_components/types.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 237
|
||||
"loc": 238
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -553,12 +573,12 @@
|
||||
"client/src/launcher/ui_runtime/control_paths.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 302
|
||||
"loc": 316
|
||||
},
|
||||
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 5,
|
||||
"loc": 273
|
||||
"loc": 277
|
||||
},
|
||||
"client/src/launcher/ui_runtime/log_filtering.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -568,7 +588,7 @@
|
||||
"client/src/launcher/ui_runtime/process_logs.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 7,
|
||||
"loc": 277
|
||||
"loc": 279
|
||||
},
|
||||
"client/src/launcher/ui_runtime/report_popouts.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -588,7 +608,7 @@
|
||||
"client/src/launcher/ui_runtime/status_refresh.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 3,
|
||||
"loc": 444
|
||||
"loc": 446
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -918,7 +938,7 @@
|
||||
"server/src/audio/ear_capture.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 9,
|
||||
"loc": 450
|
||||
"loc": 451
|
||||
},
|
||||
"server/src/audio/ear_capture/source_watchdog.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -1253,7 +1273,7 @@
|
||||
"server/src/uvc_runtime.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 4,
|
||||
"loc": 255
|
||||
"loc": 264
|
||||
},
|
||||
"server/src/video.rs": {
|
||||
"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 {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
|
||||
loop {
|
||||
if let Ok(contents) = fs::read_to_string(marker) {
|
||||
if is_ready(&contents) {
|
||||
return contents;
|
||||
}
|
||||
if let Ok(contents) = fs::read_to_string(marker)
|
||||
&& is_ready(&contents)
|
||||
{
|
||||
return contents;
|
||||
}
|
||||
assert!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user