Skip to content
Open
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
312 changes: 312 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ repository = "https://github.com/sernauer/breakwater"

[workspace.dependencies]
async-trait = "0.1"
axum = { version = "0.8", features = ["ws"] }
bytemuck = { version = "1.21" }
bytes = "1.0"
chrono = "0.4"
clap = { version = "4.5", features = ["derive"] }
color-eyre = "0.6"
const_format = "0.2"
criterion = { version = "0.8", features = ["async_tokio"] }
eframe = { version = "0.34", default-features = false, features = ["glow", "default_fonts", "persistence", "wayland", "x11"] }
egui = "0.34"
flate2 = "1.0"
futures = "0.3"
libloading = "0.9"
local-ip-address = "0.6.3"
Expand Down
6 changes: 5 additions & 1 deletion breakwater/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ breakwater-parser.workspace = true
breakwater-egui-overlay = { workspace = true, optional = true }

async-trait.workspace = true
axum = { workspace = true, optional = true }
bytemuck = { workspace = true, optional = true }
bytes = { workspace = true, optional = true }
chrono.workspace = true
clap.workspace = true
color-eyre.workspace = true
const_format.workspace = true
eframe = { workspace = true, optional = true }
egui = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true }
libloading = { workspace = true, optional = true }
local-ip-address.workspace = true
memadvise.workspace = true
Expand All @@ -48,14 +51,15 @@ rstest.workspace = true

[features]
# We don't enable binary-sync-pixels and binary-set-pixel by default to make it a bit harder for clients ;)
default = ["egui", "vnc"]
default = ["egui", "vnc", "web"]

alpha = ["breakwater-parser/alpha"]
binary-set-pixel = ["breakwater-parser/binary-set-pixel"]
binary-sync-pixels = ["breakwater-parser/binary-sync-pixels"]
egui = ["dep:breakwater-egui-overlay", "dep:bytemuck", "dep:eframe", "dep:egui", "dep:libloading"]
native-display = ["dep:softbuffer", "dep:winit"]
vnc = ["dep:vncserver"]
web = ["dep:axum", "dep:bytes", "dep:flate2"]

[lints]
workspace = true
43 changes: 42 additions & 1 deletion breakwater/src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub struct CliArgs {
pub viewport: Vec<crate::sinks::egui::ViewportConfig>,

/// Specify one or more pixelflut endpoints to display.
#[cfg(feature = "egui")]
#[cfg(any(feature = "egui", feature = "web"))]
#[clap(long)]
pub advertised_endpoints: Vec<String>,

Expand All @@ -112,6 +112,17 @@ pub struct CliArgs {
#[clap(long)]
pub ui: Option<std::path::PathBuf>,

/// Listen address for the WebUI, e.g. `[::]:8080`.
/// Serves a small website that streams the canvas to web browsers over a WebSocket.
#[cfg(feature = "web")]
#[clap(long)]
pub web_listen_address: Option<SocketAddr>,

/// Maximum number of chat messages a single IP address may send per minute in the WebUI.
#[cfg(feature = "web")]
#[clap(long, default_value_t = 10)]
pub chat_messages_per_minute: u32,

/// Create (or use an existing) shared memory region for the framebuffer.
/// This enables other applications to read and write Pixel values to the framebuffer or can be
/// used to persist the canvas across restarts.
Expand All @@ -120,6 +131,36 @@ pub struct CliArgs {
}

impl CliArgs {
/// Resolves the Pixelflut endpoints to advertise to users (so they know where to connect).
///
/// If `--advertised-endpoints` is set, those are returned verbatim. Otherwise we make a best
/// effort to guess: for a single listener we resolve the local v4 + v6 IPs and append the port,
/// for multiple listeners we just list them.
#[cfg(any(feature = "egui", feature = "web"))]
pub fn resolve_advertised_endpoints(&self) -> Vec<String> {
if !self.advertised_endpoints.is_empty() {
return self.advertised_endpoints.clone();
}

match &self.listen_addresses[..] {
// No listeners given, so also no endpoints to advertise
[] => vec![],
// In case of a single listener we get the local IPs (v4 + v6) and concat them with the
// port
[single_listener] => {
let port = single_listener.port();

[local_ip_address::local_ip(), local_ip_address::local_ipv6()]
.into_iter()
.filter_map(Result::ok)
.map(|ip| format!("{ip}:{port}"))
.collect()
}
// If multiple listeners are used it's complicated, so we just print them
multiple_listeners => multiple_listeners.iter().map(ToString::to_string).collect(),
}
}

/// Checks that at most one IP per version (v4/v6) is configured.
/// Returns the (optional) v4 address and (optional) v6 address.
#[cfg(feature = "vnc")]
Expand Down
18 changes: 18 additions & 0 deletions breakwater/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ async fn main() -> eyre::Result<()> {
}
}

#[cfg(feature = "web")]
{
use crate::sinks::web::WebSink;

if let Some(web_sink) = WebSink::new(
fb.clone(),
&args,
statistics_tx.clone(),
statistics_information_rx.resubscribe(),
terminate_signal_rx.resubscribe(),
)
.await
.context("failed to create web sink")?
{
display_sinks.push(Box::new(web_sink));
}
}

let mut ffmpeg_thread_present = false;
if let Some(ffmpeg_sink) = FfmpegSink::new(
fb.clone(),
Expand Down
24 changes: 1 addition & 23 deletions breakwater/src/sinks/egui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,7 @@ impl<FB: FrameBuffer + Send + Sync + 'static> DisplaySink<FB> for EguiSink<FB> {
}
});

let advertised_endpoints = if cli_args.advertised_endpoints.is_empty() {
// In case no advertised endpoints to display are given, we calculate the most likely
// endpoint(s) to display.
match &cli_args.listen_addresses[..] {
// No listeners given, so also no endpoints to advertise
[] => vec![],
// In case of a single listener we get the local IPs (v4 + v6) and concat them with
// the port
[single_listener] => {
let port = single_listener.port();

[local_ip_address::local_ip(), local_ip_address::local_ipv6()]
.into_iter()
.filter_map(Result::ok)
.map(|ip| format!("{ip}:{port}"))
.collect()
}
// If multiple listeners are used it's complicated, so we just print them
multiple_listeners => multiple_listeners.iter().map(ToString::to_string).collect(),
}
} else {
cli_args.advertised_endpoints.clone()
};
let advertised_endpoints = cli_args.resolve_advertised_endpoints();

Ok(Some(Self {
framebuffer: fb,
Expand Down
2 changes: 2 additions & 0 deletions breakwater/src/sinks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod ffmpeg;
pub mod native_display;
#[cfg(feature = "vnc")]
pub mod vnc;
#[cfg(feature = "web")]
pub mod web;

// The stabilization of async functions in traits in Rust 1.75 did not include support for using traits containing async
// functions as dyn Trait, so we still need to use async_trait here.
Expand Down
Loading
Loading