Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions host/src/capture/dxgi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub fn run_capture_loop(
slot: &FrameSlot,
shared: SharedControl,
adapter_index: u32,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// --- Create DXGI factory and select the output to capture ---
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1()? };
Expand Down Expand Up @@ -272,7 +273,9 @@ pub fn run_capture_loop(
let mut cursor_y: i32 = 0;

loop {
if !shared.running.load(Ordering::SeqCst) {
if !shared.running.load(Ordering::SeqCst)
|| !crate::supervisor::generation_is_current(generation)
{
info!("Capture loop stopping on running=false");
break;
}
Expand Down Expand Up @@ -445,7 +448,9 @@ pub fn run_capture_loop(
}
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
// Desktop unchanged — not an error.
if !shared.running.load(Ordering::SeqCst) {
if !shared.running.load(Ordering::SeqCst)
|| !crate::supervisor::generation_is_current(generation)
{
info!("Capture loop stopping after timeout on running=false");
break;
}
Expand Down Expand Up @@ -489,7 +494,9 @@ pub fn run_capture_loop(
let mut reinit = None;
let delays_ms = [100u64, 250, 500, 1000, 2000];
for attempt in 0..15 {
if !shared.running.load(Ordering::SeqCst) {
if !shared.running.load(Ordering::SeqCst)
|| !crate::supervisor::generation_is_current(generation)
{
break;
}
// Keep the watchdog quiet: this wait IS the recovery.
Expand Down
14 changes: 8 additions & 6 deletions host/src/capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ fn vdd_attach_timeout() -> Duration {
/// a client is connected, so an idle PC never shows a phantom second monitor.
#[cfg(windows)]
pub(crate) fn client_connected(shared: &SharedControl) -> bool {
let addr = *shared.target_addr.lock();
!addr.ip().is_unspecified() && addr.port() != 0
shared.client_connected()
}

/// Reconcile the virtual display device to the requested target and return the concrete
Expand Down Expand Up @@ -310,6 +309,7 @@ pub fn run_capture_stage(
slot: FrameSlot,
shared: SharedControl,
adapter_index: u32,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
struct CloseOnExit(FrameSlot);
impl Drop for CloseOnExit {
Expand All @@ -318,7 +318,7 @@ pub fn run_capture_stage(
}
}
let guard = CloseOnExit(slot.handle());
let result = run_platform_loop(&guard.0, shared, adapter_index);
let result = run_platform_loop(&guard.0, shared, adapter_index, generation);
if let Err(ref e) = result {
error!(error = %e, "Capture loop exited with error");
}
Expand All @@ -337,13 +337,14 @@ fn run_platform_loop(
slot: &FrameSlot,
shared: SharedControl,
adapter_index: u32,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let synthetic_requested =
std::env::var("ETERNAL_CAPTURE").is_ok_and(|v| v.trim().eq_ignore_ascii_case("synthetic"));
if synthetic_requested {
synthetic::run_capture_loop(slot, shared)
synthetic::run_capture_loop(slot, shared, generation)
} else {
dxgi::run_capture_loop(slot, shared, adapter_index)
dxgi::run_capture_loop(slot, shared, adapter_index, generation)
}
}

Expand All @@ -352,8 +353,9 @@ fn run_platform_loop(
slot: &FrameSlot,
shared: SharedControl,
_adapter_index: u32,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
synthetic::run_capture_loop(slot, shared)
synthetic::run_capture_loop(slot, shared, generation)
}

/// Choose the freshly-attached virtual output: the first non-primary output whose device name was
Expand Down
7 changes: 5 additions & 2 deletions host/src/capture/synthetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fn synthetic_size() -> (u32, u32) {
pub fn run_capture_loop(
slot: &FrameSlot,
shared: SharedControl,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (width, height) = synthetic_size();
let row_bytes = (width * 4) as usize;
Expand Down Expand Up @@ -78,8 +79,10 @@ pub fn run_capture_loop(
let mut next_deadline = Instant::now();

loop {
if !shared.running.load(Ordering::SeqCst) {
info!("Capture loop stopping on running=false");
if !shared.running.load(Ordering::SeqCst)
|| !crate::supervisor::generation_is_current(generation)
{
info!("Capture loop stopping (stop requested or generation superseded)");
break;
}

Expand Down
28 changes: 28 additions & 0 deletions host/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ impl SharedControl {
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}

/// Is a client actually watching? Protocol v2 makes the session the only
/// truth: media needs a session id, so a target address on its own (a
/// persisted `target_ip`, a stale value from a previous client) must never
/// read as "someone is connected" — that would keep the capture loop at
/// full rate with nobody watching and bring the virtual display up with no
/// viewer.
pub fn client_connected(&self) -> bool {
self.session.lock().is_active()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -175,3 +185,21 @@ impl GuiControl {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn client_connected_requires_a_session_not_just_an_address() {
let shared = SharedControl::new(9876, 15_000_000);
assert!(!shared.client_connected(), "no session, no client");

// A target address on its own — a persisted setting, or a leftover
// from a client that has since gone — must not read as "connected".
// It used to, which held the virtual display up and kept the capture
// loop at full rate with nobody watching.
*shared.target_addr.lock() = "192.168.1.50:9876".parse().unwrap();
assert!(!shared.client_connected());
}
}
10 changes: 7 additions & 3 deletions host/src/encoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ pub fn run_encode_stage(
tx: mpsc::Sender<NALUnit>,
shared: SharedControl,
gpu: GpuInfo,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = run_encode_loop(frames, tx, shared, gpu);
let result = run_encode_loop(frames, tx, shared, gpu, generation);
if let Err(ref e) = result {
error!(error = %e, "Encode loop exited with error");
}
Expand All @@ -58,6 +59,7 @@ fn run_encode_loop(
tx: mpsc::Sender<NALUnit>,
shared: SharedControl,
gpu: GpuInfo,
generation: u64,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Apply GUI encoder override at pipeline start. The override is consulted only here;
// mid-stream changes don't take effect until the user requests a Restart.
Expand Down Expand Up @@ -130,8 +132,10 @@ fn run_encode_loop(
.and_then(|v| v.trim().parse().ok());

while let Some(raw_frame) = frames.blocking_take() {
if !shared.running.load(Ordering::SeqCst) {
info!("Encoder loop stopping on running=false");
if !shared.running.load(Ordering::SeqCst)
|| !crate::supervisor::generation_is_current(generation)
{
info!("Encoder loop stopping (stop requested or generation superseded)");
break;
}

Expand Down
88 changes: 18 additions & 70 deletions host/src/gui.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::net::SocketAddr;

use eframe::egui;
use qrcode::{Color as QrModuleColor, QrCode};
use tracing::{info, warn};
Expand Down Expand Up @@ -145,8 +143,9 @@ pub struct AnalyzerApp {
current_tab: AppTab,
settings_bitrate_mbps: f32,
settings_fps_target: u32,
settings_target_ip: String,
settings_target_error: Option<String>,
/// Last settings error worth showing (currently only the autostart
/// registry write).
settings_error: Option<String>,
settings_start_on_boot: bool,
settings_hevc_enabled: bool,
settings_vdd_match: bool,
Expand Down Expand Up @@ -199,21 +198,6 @@ impl AnalyzerApp {
.target_fps
.store(fps_target, std::sync::atomic::Ordering::SeqCst);

let settings_target_ip = if let Some(ip) = persisted.target_ip.clone() {
if let Ok(addr) = ip.parse::<SocketAddr>() {
*control.shared.target_addr.lock() = addr;
PIPELINE_STATS.lock().set_target_addr(addr.to_string());
}
ip
} else {
let target_addr = *control.shared.target_addr.lock();
if target_addr.ip().is_unspecified() || target_addr.port() == 0 {
String::new()
} else {
target_addr.to_string()
}
};

let encoder_choice = if let Some(name) = persisted.encoder_override.as_deref() {
*control.shared.encoder_override.lock() = Some(name.to_string());
ENCODER_CHOICES
Expand Down Expand Up @@ -271,8 +255,7 @@ impl AnalyzerApp {
current_tab: AppTab::Stream,
settings_bitrate_mbps: bitrate_mbps,
settings_fps_target: fps_target,
settings_target_ip,
settings_target_error: None,
settings_error: None,
settings_start_on_boot: start_on_boot,
settings_hevc_enabled: persisted.hevc_enabled,
settings_vdd_match: persisted.vdd_match_resolution,
Expand All @@ -285,24 +268,6 @@ impl AnalyzerApp {
}
}

fn apply_target_addr(&mut self) {
match self.settings_target_ip.trim().parse::<SocketAddr>() {
Ok(target_addr) => {
*self.control.shared.target_addr.lock() = target_addr;
PIPELINE_STATS
.lock()
.set_target_addr(target_addr.to_string());
self.settings_target_error = None;
info!(target = %target_addr, "Transport target updated from GUI");
self.persist_settings();
}
Err(error) => {
self.settings_target_error = Some("Enter host:port".to_string());
warn!(error = %error, target = %self.settings_target_ip, "Invalid target address");
}
}
}

fn persist_settings(&self) {
let encoder_override = ENCODER_CHOICES
.iter()
Expand All @@ -317,11 +282,9 @@ impl AnalyzerApp {
let file = SettingsFile {
bitrate_mbps: self.settings_bitrate_mbps,
target_fps: self.settings_fps_target,
target_ip: if self.settings_target_ip.trim().is_empty() {
None
} else {
Some(self.settings_target_ip.trim().to_string())
},
// v2 has no manual target; the field stays in the file only so
// older settings.json still parse.
target_ip: None,
encoder_override,
capture_display: if self.settings_capture_display.is_empty() {
None
Expand Down Expand Up @@ -381,13 +344,6 @@ impl eframe::App for AnalyzerApp {
ctx.set_visuals(visuals);

let snap = StatsSnapshot::take();
if self.settings_target_ip.is_empty()
&& !snap.target_addr.is_empty()
&& snap.target_addr != "0.0.0.0:9876"
{
self.settings_target_ip = snap.target_addr.clone();
}

self.draw_sidebar(ctx, &snap);

egui::CentralPanel::default()
Expand Down Expand Up @@ -762,23 +718,12 @@ impl AnalyzerApp {

ui.add_space(12.0);

// --- Target IP --------------------------------------------------------
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Target IP").color(TEXT).size(13.0));
let response = ui.add(
egui::TextEdit::singleline(&mut self.settings_target_ip).desired_width(220.0),
);
let enter_pressed =
response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
if amber_button(ui, "Apply").clicked() || enter_pressed {
self.apply_target_addr();
}
});
if let Some(error) = &self.settings_target_error {
ui.label(egui::RichText::new(error).color(RED).size(11.0));
}

ui.add_space(12.0);
// Protocol v2 has no manual target: media carries the session id
// minted by the HELLO2 handshake, so an address typed here could
// never receive a stream. The old field also wrote target_addr,
// which the capture loop read as "a client is watching" — enough
// to hold the virtual display up and keep capture at full rate
// with nobody connected.

// --- Encoder override dropdown ---------------------------------------
let detected = PIPELINE_STATS.lock().codec_name.clone();
Expand Down Expand Up @@ -962,12 +907,15 @@ impl AnalyzerApp {
if self.settings_start_on_boot != prev_boot {
if let Err(error) = set_startup_registry(self.settings_start_on_boot) {
self.settings_start_on_boot = prev_boot;
self.settings_target_error = Some(error);
self.settings_error = Some(error);
} else {
self.settings_target_error = None;
self.settings_error = None;
self.persist_settings();
}
}
if let Some(error) = &self.settings_error {
ui.label(egui::RichText::new(error).color(RED).size(11.0));
}
}
});

Expand Down
15 changes: 10 additions & 5 deletions host/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ pub async fn run_pipeline_supervised(
let capture_thread = std::thread::Builder::new()
.name(format!("capture-g{generation}"))
.spawn(move || {
let outcome =
match capture::run_capture_stage(frame_producer, capture_shared, adapter_index) {
Ok(()) => StageOutcome::Completed,
Err(e) => StageOutcome::Failed(e.to_string()),
};
let outcome = match capture::run_capture_stage(
frame_producer,
capture_shared,
adapter_index,
generation,
) {
Ok(()) => StageOutcome::Completed,
Err(e) => StageOutcome::Failed(e.to_string()),
};
capture_reporter.stage_exited(Stage::Capture, outcome);
});
if let Err(e) = capture_thread {
Expand All @@ -69,6 +73,7 @@ pub async fn run_pipeline_supervised(
nal_tx,
encoder_shared,
encoder_gpu,
generation,
) {
Ok(()) => StageOutcome::Completed,
Err(e) => StageOutcome::Failed(e.to_string()),
Expand Down
Loading
Loading