Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32131b0ccb | ||
|
|
035aa4b256 | ||
|
|
f85c0c22c7 | ||
|
|
c30418a0f5 | ||
|
|
14245cbf08 |
+33
-1
@@ -65,9 +65,41 @@ pub fn measure_upstream_blocking() -> Result<Measurement> {
|
|||||||
/// via SAFETY_FACTOR) into a recommended viewer count. Floors to at least 1.
|
/// via SAFETY_FACTOR) into a recommended viewer count. Floors to at least 1.
|
||||||
pub fn recommended_max_viewers(safe_mbps: f64, bitrate_kbps: u32) -> u32 {
|
pub fn recommended_max_viewers(safe_mbps: f64, bitrate_kbps: u32) -> u32 {
|
||||||
let per_viewer_mbps = (bitrate_kbps as f64) / 1000.0;
|
let per_viewer_mbps = (bitrate_kbps as f64) / 1000.0;
|
||||||
if per_viewer_mbps <= 0.0 {
|
// Guard non-finite / non-positive inputs (only reachable from a corrupted
|
||||||
|
// config): a NaN safe_mbps would cast to 0 and an infinite one to u32::MAX,
|
||||||
|
// both of which break the "at least 1" contract.
|
||||||
|
if !safe_mbps.is_finite() || safe_mbps <= 0.0 || per_viewer_mbps <= 0.0 {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
let n = (safe_mbps / per_viewer_mbps).floor();
|
let n = (safe_mbps / per_viewer_mbps).floor();
|
||||||
if n < 1.0 { 1 } else { n as u32 }
|
if n < 1.0 { 1 } else { n as u32 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::recommended_max_viewers;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn divides_bandwidth_by_per_viewer_bitrate() {
|
||||||
|
// 8 Mbps safe / 2 Mbps each = 4 viewers.
|
||||||
|
assert_eq!(recommended_max_viewers(8.0, 2000), 4);
|
||||||
|
// Floors the fractional part: 7.9 / 2 = 3.95 -> 3.
|
||||||
|
assert_eq!(recommended_max_viewers(7.9, 2000), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floors_to_at_least_one() {
|
||||||
|
// Not even enough for one viewer still allows one (best effort).
|
||||||
|
assert_eq!(recommended_max_viewers(0.5, 2000), 1);
|
||||||
|
// Zero / unknown bitrate can't size a budget; floor to one.
|
||||||
|
assert_eq!(recommended_max_viewers(8.0, 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn degenerate_inputs_floor_to_one() {
|
||||||
|
// A corrupted config must not yield 0 (NaN) or u32::MAX (Inf).
|
||||||
|
assert_eq!(recommended_max_viewers(f64::NAN, 2000), 1);
|
||||||
|
assert_eq!(recommended_max_viewers(f64::INFINITY, 2000), 1);
|
||||||
|
assert_eq!(recommended_max_viewers(-5.0, 2000), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-3
@@ -7,10 +7,17 @@ pub fn install_ctrl_c() -> CancellationToken {
|
|||||||
let token = CancellationToken::new();
|
let token = CancellationToken::new();
|
||||||
let trigger = token.clone();
|
let trigger = token.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if tokio::signal::ctrl_c().await.is_ok() {
|
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||||
tracing::info!("ctrl-c received, shutting down");
|
// Installing the handler failed — ctrl-c won't trigger a graceful
|
||||||
trigger.cancel();
|
// shutdown. Say so instead of failing silently; the user can still
|
||||||
|
// kill the process, and the second-ctrl-c arm below would only fail
|
||||||
|
// the same way, so bail out of the task.
|
||||||
|
tracing::warn!("could not install ctrl-c handler: {e}; ctrl-c won't shut down cleanly");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
tracing::info!("ctrl-c received, shutting down");
|
||||||
|
trigger.cancel();
|
||||||
|
|
||||||
if tokio::signal::ctrl_c().await.is_ok() {
|
if tokio::signal::ctrl_c().await.is_ok() {
|
||||||
tracing::warn!("second ctrl-c — exiting now");
|
tracing::warn!("second ctrl-c — exiting now");
|
||||||
std::process::exit(130);
|
std::process::exit(130);
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ struct PixelPassTray {
|
|||||||
/// Wakes the winit loop and delivers the action — works even when the
|
/// Wakes the winit loop and delivers the action — works even when the
|
||||||
/// window has been dropped to the tray (no egui frame is running then).
|
/// window has been dropped to the tray (no egui frame is running then).
|
||||||
proxy: EventLoopProxy<UserEvent>,
|
proxy: EventLoopProxy<UserEvent>,
|
||||||
|
/// Shared with [`TrayHandle`]; kept in sync with the watcher's presence via
|
||||||
|
/// the `watcher_online`/`watcher_offline` callbacks so the app never diverts
|
||||||
|
/// a close to a tray that has since disappeared.
|
||||||
|
registered: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PixelPassTray {
|
impl PixelPassTray {
|
||||||
@@ -131,6 +135,25 @@ impl ksni::Tray for PixelPassTray {
|
|||||||
self.notify(TrayAction::Show);
|
self.notify(TrayAction::Show);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The StatusNotifierWatcher came back (e.g. the panel restarted). Mark the
|
||||||
|
/// tray live again so close-to-tray can resume hiding the window.
|
||||||
|
fn watcher_online(&self) {
|
||||||
|
self.registered.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The watcher went away (panel restart, tray plugin disabled, …). Clear the
|
||||||
|
/// flag so a subsequent close quits normally instead of destroying the window
|
||||||
|
/// into a tray that no longer exists, and force the window back now in case
|
||||||
|
/// it was already hidden (otherwise it'd be stranded with no way to restore).
|
||||||
|
/// Returning `true` keeps the service alive so it re-registers if the watcher
|
||||||
|
/// returns.
|
||||||
|
fn watcher_offline(&self, reason: ksni::OfflineReason) -> bool {
|
||||||
|
tracing::warn!("tray: StatusNotifierWatcher offline ({reason:?}); restoring window");
|
||||||
|
self.registered.store(false, Ordering::Release);
|
||||||
|
self.notify(TrayAction::Show);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
|
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
|
||||||
use ksni::menu::{MenuItem, StandardItem};
|
use ksni::menu::{MenuItem, StandardItem};
|
||||||
vec.
|
||||||
|
pub fn shutdown(mut self) {
|
||||||
|
self.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Routing {
|
impl Drop for Routing {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(router) = self.stream_router.take() {
|
self.cleanup();
|
||||||
router.shutdown();
|
|
||||||
}
|
|
||||||
if let Some(task) = self.event_task.take() {
|
|
||||||
task.abort();
|
|
||||||
}
|
|
||||||
if let Some(id) = self.loopback_module.lock().unwrap().take() {
|
|
||||||
unload_module(id);
|
|
||||||
}
|
|
||||||
if let Some(id) = self.sink_module.take() {
|
|
||||||
unload_module(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-25
@@ -50,34 +50,41 @@ pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let (quic_send, quic_recv) = conn.open_bi().await?;
|
// Everything past the established connection runs in one block so any error
|
||||||
|
// (open_bi, bind, local_addr, accept) is captured rather than `?`-propagated
|
||||||
|
// straight out of the function — that would skip the close below and leak the
|
||||||
|
// endpoint. The connect-phase arms above close explicitly for the same reason.
|
||||||
|
let result = async {
|
||||||
|
let (quic_send, quic_recv) = conn.open_bi().await?;
|
||||||
|
|
||||||
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
||||||
let port = listener.local_addr()?.port();
|
let port = listener.local_addr()?.port();
|
||||||
let url = format!("http://127.0.0.1:{port}");
|
let url = format!("http://127.0.0.1:{port}");
|
||||||
output::emit(output::Event::Connected { url: &url });
|
output::emit(output::Event::Connected { url: &url });
|
||||||
|
|
||||||
if opts.interactive {
|
if opts.interactive {
|
||||||
let player = crate::interactive::prompt_player()?;
|
let player = crate::interactive::prompt_player()?;
|
||||||
player
|
player
|
||||||
.spawn(&url)
|
.spawn(&url)
|
||||||
.with_context(|| "failed to launch player")?;
|
.with_context(|| "failed to launch player")?;
|
||||||
print_viewer_banner_interactive();
|
print_viewer_banner_interactive();
|
||||||
} else {
|
} else {
|
||||||
print_viewer_banner(&url);
|
print_viewer_banner(&url);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
accepted = listener.accept() => {
|
||||||
|
let (tcp, peer) = accepted?;
|
||||||
|
tracing::info!(%peer, "local viewer connected");
|
||||||
|
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
|
||||||
|
}
|
||||||
|
_ = cancel.cancelled() => {
|
||||||
|
tracing::info!("ctrl-c received before local viewer connected");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.await;
|
||||||
let result = tokio::select! {
|
|
||||||
accepted = listener.accept() => {
|
|
||||||
let (tcp, peer) = accepted?;
|
|
||||||
tracing::info!(%peer, "local viewer connected");
|
|
||||||
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
|
|
||||||
}
|
|
||||||
_ = cancel.cancelled() => {
|
|
||||||
tracing::info!("ctrl-c received before local viewer connected");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
endpoint.close().await;
|
endpoint.close().await;
|
||||||
result
|
result
|
||||||
|
|||||||
Reference in New Issue
Block a user