diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..5760de9 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,60 @@ +on: + push: + branches: [main, docs] + +name: Build documentation site +jobs: + build: + name: Build + runs-on: ubuntu-24.04 + steps: + - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v4 + + - name: generate config reference + run: | + mkdir -p docs/content/reference + cargo run --manifest-path mm-docgen/Cargo.toml --bin config-docgen \ + mmserver.default.toml > docs/content/reference/config.md + + - name: generate protocol reference + run: | + cargo run --manifest-path mm-docgen/Cargo.toml --bin protocol-docgen \ + mm-protocol/src/messages.proto > docs/content/reference/protocol.md + + - name: zola build + run: zola -r docs build -o docs/build + + - name: generate rustdoc for mm-protocol + run: | + cargo doc --manifest-path mm-protocol/Cargo.toml \ + --no-deps --target-dir docs/build + + - name: generate rustdoc for mm-client-common + run: | + cargo doc --manifest-path mm-client-common/Cargo.toml \ + --no-deps --target-dir docs/build + + - name: Upload static files + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + + + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2a93216 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/themes/anemone"] + path = docs/themes/anemone + url = https://github.com/Speyll/anemone diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b20b80c --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +# autogenerated +content/reference +build/ diff --git a/docs/config.toml b/docs/config.toml new file mode 100644 index 0000000..84a379d --- /dev/null +++ b/docs/config.toml @@ -0,0 +1,15 @@ +base_url = "https://colinmarc.github.io/magic-mirror" +theme = "anemone" +compile_sass = false +build_search_index = false + +[markdown] +highlight_code = true + +[extra] +twitter_card = false +header_nav = [ + { url = "/", name_en = "/home/"}, + { url = "https://github.com/colinmarc/magic-mirror", name_en = "/github/"}, + { url = "", name_en = "/discord/"}, +] diff --git a/docs/content/_index.md b/docs/content/_index.md new file mode 100644 index 0000000..a2f637a --- /dev/null +++ b/docs/content/_index.md @@ -0,0 +1,40 @@ ++++ ++++ + +# Magic Mirror 🪞✨ + +This page contains documentation for [Magic Mirror](github.com/colinmarc/magic-mirror), +an open-source game streaming and remote desktop tool for linux hosts. + +### Download + +These links always point to the latest release. + + - 💾 [Server [mmserver-0.5.6]](https://github.com/colinmarc/magic-mirror/releases/tag/mmserver-v0.5.6) + - 💾 [Command-Line Client [mmclient-0.4.1]](https://github.com/colinmarc/magic-mirror/releases/tag/mmclient-v0.4.1) + - 💾 [macOS GUI Client](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest) + +### Setup Guides + +Start here to get things up and running. + + - ⚙️ [Server Setup](@/setup/server.md) + - ⚙️ [Client Setup](@/setup/client.md) + + + +### Reference + +Autogenerated from the code. + + - 📖 [Configuration Reference](@/reference/config.md) + - 📖 [Protocol Reference](@/reference/protocol.md) + - 📖 [Rustdoc for `mm-protocol`](./doc/mm_protocol) + - 📖 [Rustdoc for `mm-client-common`](./doc/mm_client_common) + +### Resources + +Get help, report issues, make friends. + + - ⁉️ [Issue Tracker](https://github.com/colinmarc/magic-mirror/issues) + - 💬 [Discord Chat](https://discord.gg/v22G644DzS) diff --git a/docs/content/setup/client.md b/docs/content/setup/client.md new file mode 100644 index 0000000..b3a451c --- /dev/null +++ b/docs/content/setup/client.md @@ -0,0 +1,50 @@ ++++ +title = "Client Setup" + +[extra] +toc = true ++++ + +## macOS GUI Client + +The native macOS client can be downloaded from [the releases page](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest). + +It should work out of the box on ARM and Intel Macs running macOS 10.14 or +later. + +## Installing the commandline client + +There is also a cross-platform commandline client, `mmclient`. You can download +it [here](https://github.com/colinmarc/magic-mirror/releases/tag/mmclient-v0.4.1). + +The commandline client requires `ffmpeg` 6.0 or later to be installed on the +system. It also requires up-to-date Vulkan drivers. + +## Building mmclient + +The following are required to build the client and its dependencies: + +``` +rust (MSRV 1.77.2) +nasm +cmake +protoc +libxkbcommon (linux only) +libwayland-client (linux only) +alsa (linux only) +ffmpeg 6.x +``` + +Besides Rust itself, the following command will install everything on ubuntu: + +``` +apt install \ + nasm cmake protobuf-compiler libxkbcommon-dev libwayland-dev libasound2-dev \ + ffmpeg libavutil-dev libavformat-dev libavdevice-dev libavfilter-dev +``` + +Or using homebrew on macOS: + +``` +brew install nasm cmake ffmpeg@6 protobuf +``` diff --git a/docs/content/setup/server.md b/docs/content/setup/server.md new file mode 100644 index 0000000..aad36b7 --- /dev/null +++ b/docs/content/setup/server.md @@ -0,0 +1,134 @@ ++++ +title = "Server Setup" + +[extra] +toc = true ++++ + +## Quickstart + +First, grab [the latest server release](https://github.com/colinmarc/magic-mirror/releases/tag/mmserver-v0.5.6) and untar it somewhere: + +```sh +curl -fsSL "https://github.com/colinmarc/magic-mirror/releases/download/mmserver-v0.5.6/mmserver-v0.5.6-linux-amd64.tar.gz" \ + | tar zxv +cd mmserver-0.5.6 +``` + +Then, create a [configuration file](@/reference/config.md) with at least one application definition: + +```toml +# mmserver.toml +[apps.steam-gamepadui] +command = ["steam", "-gamepadui"] +xwayland = true +``` + +Then you can start the server like so: + +``` +$ ./mmserver -C config.toml +2024-12-09T16:57:30.989261Z INFO mmserver: listening on [::1]:9599 +``` + +You can also create a configuration directory, and add a file (json or toml) for each application: + +```sh +mkdir apps.d +echo 'command = ["steam", "-gamepadui"]' > apps.d/steam.toml +./mmserver -i apps.d +``` + +## Connectivity + +By default, mmserver only listens on `localhost`, which is not terribly +useful. There are a few different options to configure which socket address the +server listens for connections on. + +The easiest is to bind to a local IP, or use a VPN like wireguard or tailscale: + +```toml +# config.toml +[server] +bind = "192.168.1.37" +``` + +Or from the command line: + +```sh +mmserver --bind $(tailscale ip -4):9599 +``` + +If you'd like to stream on a public IP, or on all interfaces (with `0.0.0.0`), +mmserver requires that you set up a TLS certificate and key: + +```toml +# config.toml +[server] +tls_cert = "/path/to/tls.key" +tls_key = "/path/to/tls.cert" +``` + +Generating such certificates and adding them to the client is out of scope for +this guide. Note that while all Magic Mirror traffic is encrypted with TLS +(whether you supply certificates or not), no _authentication_ is performed on +incoming connections. + +Finally, you can also use `--bind-systemd` or `bind_systemd = true` to bind to a +[systemd socket](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html). + +## Hardware/software encoding + +Magic Mirror uses video compression codecs to stream the game over the wire. +Hardware encoding using the GPU is needed to get the best performance and latency. +CPU-based encode is available as a fallback, but it's slower, albeit slightly +higher quality. + +To see if your GPU supports video encoding, see the following matrix for your vendor: + - [AMD](https://en.wikipedia.org/wiki/Unified_Video_Decoder#Format_support) + - [NVIDIA](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new) + +Note that with the `ffmpeg_encode` feature, linking against a system-installed `ffmpeg` +is supported, which may allow you to use specific CPU-based codecs not considered +in this table. + +| Codec | CPU | AMD | NVIDIA | Intel | +| ----- | :-: | :-: | :----: | :---: | +| H.264 | ❌ | ✅ | ✅ | ❔ | +| H.265 | ✅ | ✅ | ✅ | ❔ | +| AV1 | ✅ | ❌ | ❌ | ❌ | + +## Building `mmserver` from source + +The following are required to build the server and its dependencies: + +``` +rust (MSRV 1.77.2) +nasm +cmake +protoc +libxkbcommon +``` + +Besides Rust itself, the following command will install everything on ubuntu: + +``` +apt install nasm cmake protobuf-compiler libxkbcommon-dev +``` + +Then you should be good to go: + +``` +cd mm-server +cargo build --bin mmserver [--release] +``` + +### Feature flags + +The following feature flags are available: + + - `vulkan_encode` (on by default) - enables hardware encode + - `svt_encode` (on by default) - enables svt-av1 and svt-hevc for CPU encode + - `ffmpeg_encode` - allows using system-installed ffmpeg to do CPU encode + +Note that `ffmpeg_encode` takes precedence over `svt_encode` if enabled, but the server will always choose hardware encode if available on your platform. diff --git a/docs/templates/footer.html b/docs/templates/footer.html new file mode 100644 index 0000000..e69de29 diff --git a/docs/themes/anemone b/docs/themes/anemone new file mode 160000 index 0000000..ae125d2 --- /dev/null +++ b/docs/themes/anemone @@ -0,0 +1 @@ +Subproject commit ae125d2bc6297160b46bae3d230715b67c0705e9 diff --git a/mm-docgen/Cargo.lock b/mm-docgen/Cargo.lock new file mode 100644 index 0000000..6b352de --- /dev/null +++ b/mm-docgen/Cargo.lock @@ -0,0 +1,54 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mmserver-config-docgen" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" diff --git a/mm-docgen/Cargo.toml b/mm-docgen/Cargo.toml new file mode 100644 index 0000000..2c2c7ef --- /dev/null +++ b/mm-docgen/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mmserver-config-docgen" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "config-docgen" + +[[bin]] +name = "protocol-docgen" + +[dependencies] +regex = "1" diff --git a/mm-docgen/src/bin/config-docgen.rs b/mm-docgen/src/bin/config-docgen.rs new file mode 100644 index 0000000..28b0b62 --- /dev/null +++ b/mm-docgen/src/bin/config-docgen.rs @@ -0,0 +1,107 @@ +//! Generates markdown docs from mmserver.default.toml. Tightly coupled +//! to the format of that file. + +use std::{ + fs::File, + io::{BufRead as _, BufReader}, +}; + +use regex::Regex; + +const FRONT_MATTER: &str = r#" ++++ +title = "Configuration Reference" + +[extra] +toc = true ++++ +"#; + +fn main() { + let mut args = std::env::args(); + + if args.len() != 2 { + eprintln!("usage: {} SRC", args.next().unwrap()); + std::process::exit(1); + } + + let _ = args.next().unwrap(); + let src = args.next().unwrap(); + + let r = BufReader::new(File::open(src).expect("source path does not exist")); + + let mut preamble = true; + let mut key_path: Vec = Vec::new(); + let mut docs = Vec::new(); + + let keypath_section_re = Regex::new(r"\A#?\s*\[([a-z0-9-_.]+)\]\s*\z").unwrap(); + let key_re = Regex::new(r"\A(#?)\s*([a-z0-9-_]+)\s=\s(.*)\z").unwrap(); + + println!("{}", FRONT_MATTER); + + for line in r.lines() { + let s = line.expect("io error"); + if s.is_empty() { + preamble = false; + + for doc in docs.drain(..) { + println!("{}", doc); + } + + continue; + } else if preamble { + continue; + } + + if let Some(header) = s.strip_prefix("## *** ") { + // Documentation sections. + println!("\n## {}", header.strip_suffix(" ***").unwrap()); + } else if s.starts_with("## ***") { + // Section decoration. + continue; + } else if let Some(doc) = s.strip_prefix("##") { + // Key documentation. + docs.push(doc.trim_start().to_owned()); + } else if let Some(m) = key_re.captures(&s) { + // Key, value. + let is_default = m.get(1).unwrap().is_empty(); + let key = m.get(2).unwrap().as_str(); + let value = m.get(3).unwrap().as_str(); + + let full_path = key_path + .iter() + .map(String::as_str) + .chain(key.split('.')) + .collect::>() + .join("."); + + println!("\n#### `{}`\n", full_path); + if is_default { + println!("```toml\n# Default\n{} = {}\n```\n", key, value); + } else { + println!( + "```toml\n# Example (default unset)\n{} = {}\n```\n", + key, value + ); + } + + for doc in docs.drain(..) { + println!("{}", doc); + } + } else if let Some(m) = keypath_section_re.captures(&s) { + // Update keypath for TOML section headers. + key_path.clear(); + for key in m.get(1).unwrap().as_str().split(".") { + // Example app becomes in the docs. + if key == "steam-big-picture" { + key_path.push("".to_owned()); + } else { + key_path.push(key.to_owned()); + } + } + } else { + eprintln!("error: unmatched line: \n{}", s); + std::process::exit(1); + } + } +} diff --git a/mm-docgen/src/bin/protocol-docgen.rs b/mm-docgen/src/bin/protocol-docgen.rs new file mode 100644 index 0000000..be6c6c4 --- /dev/null +++ b/mm-docgen/src/bin/protocol-docgen.rs @@ -0,0 +1,61 @@ +//! Generates markdown docs from mm-protoco/src/messages.proto. Tightly coupled +//! to the format of that file. + +use std::{ + fs::File, + io::{BufRead as _, BufReader}, +}; + +const FRONT_MATTER: &str = r#" ++++ +title = "Protocol Reference" + +[extra] +toc = true ++++ +"#; + +fn main() { + let mut args = std::env::args(); + + if args.len() != 2 { + eprintln!("usage: {} SRC", args.next().unwrap()); + std::process::exit(1); + } + + let _ = args.next().unwrap(); + let src = args.next().unwrap(); + + let r = BufReader::new(File::open(src).expect("source path does not exist")); + + println!("{}", FRONT_MATTER); + + // Skip until the first

. + let mut message_lines = Vec::new(); + for line in r + .lines() + .skip_while(|s| !s.as_ref().unwrap().starts_with("// # ")) + { + let line = line.unwrap(); + if message_lines.is_empty() && line.is_empty() { + println!(); + } else if let Some(comment) = line.strip_prefix("// ").or_else(|| line.strip_prefix("//")) { + // Emit a code block. + emit_message_code_block(&mut message_lines); + + println!("{}", comment); + } else if !line.contains("TODO") { + message_lines.push(line); + } + } + + emit_message_code_block(&mut message_lines); +} + +fn emit_message_code_block(lines: &mut Vec) { + if !lines.is_empty() { + let message = lines.join("\n"); + println!("\n```proto\n{}\n```\n", message.trim()); + lines.clear(); + } +} diff --git a/mmserver.default.toml b/mmserver.default.toml index d234645..9f805ff 100644 --- a/mmserver.default.toml +++ b/mmserver.default.toml @@ -31,7 +31,7 @@ ## If you're running magic-mirror as a permanent daemon, you should set this to ## something like /var/lib/magic-mirror. ## -# data_home = /var/lib/magic-mirror +# data_home = "/var/lib/magic-mirror" ## ***-----------------*** ## *** Server Settings *** @@ -42,19 +42,19 @@ [server] ## Where the server should listen for incoming connections. IPv6 addresses are -## supported. Use 0.0.0.0 or [::] to listen on all available interfaces. +## supported. Use `0.0.0.0` or `[::]` to listen on all available interfaces. bind = "localhost:9599" ## If set, `bind` will be ignored, and the server will instead listen for ## incoming connections on the socket specified by the LISTEN_FDS environment ## variable. See the systemd documentation on "socket activation", here: -## https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html +## bind_systemd = false ## Used for TLS. Both are required unless the host portion of the bind address ## resolves to a private address (as defined by RFCs 1918, 4193, and 6598) or -## otherwise not routable, for example 127.0.0.1, 192.168.24.25, or -## fd12:3456:789a:1::1. +## otherwise not routable, for example `127.0.0.1`, `192.168.24.25`, or +## `fd12:3456:789a:1::1`. # tls_key = "/path/to/tls.key" # tls_cert = "/path/to/tls.cert" @@ -68,41 +68,45 @@ max_connections = 4 ## Whether to use mDNS to allow clients to discover the server. mdns = true -## The hostname to advertise over mDNS. Defaults to "$(uname -n).local." if left -## unset, or ignored if 'mdns = false'. +## The hostname to advertise over mDNS. Defaults to `"$(uname -n).local.` if left +## unset, or ignored if `mdns` is `false`. # mdns_hostname = "mycomputer.local." ## The instance name to advertise over mDNS. Defaults to the unqualified value of -## mdns_hostname, converted to uppercase. +## `mdns_hostname`, converted to uppercase. # mdns_instance_name = "MYCOMPUTER" ## ***-------------------------*** ## *** Configured Applications *** ## ***-------------------------*** ## -## Each application you want to stream must be configured in advance. An example -## application configuration follows. (Note that unlike the rest of this file, -## this application is not included in the default configuration.) +## Each application you want to stream must be configured in advance, with each +## application as its own section. Applications can, alternatively, be +## configured as individual files. See the documentation for `include_apps` +## above for more information. At least one application must always be +## configured. ## -## App names must be unique and only contain characters in the set [a-z0-9-_]. +## App names must be unique and only contain characters in the set `[a-z0-9-_]`. ## The section is structured as a dictionary, with the key as the application ## name. ## -## Applications can, alternatively, be configured as individual files. See the -## documentation for `include_apps` above for more information. At least one -## application must always be configured. - -## A basic app definition has a command to run. You can also specify environment -## variables to inject. +## An example application configuration follows. (Note that unlike the rest of +## this file, this application is not included in the default configuration.) # [apps.steam-big-picture] + +## A short name for the app. # description = "Steam" + +## The command to run. Must be in `$PATH` or absolute. # command = ["steam", "-gamepadui"] + +## Key/value pairs to set in the environment when running the command. # environment = { "FOO" = "bar" } ## Configure a "path" for the application. Clients can use this to group apps ## into folders. This has nothing to do with the local filesystem. Paths should ## use unix path separators. They may include characters in the set -## [A-Za-z0-9-_ ] (including spaces). +## `[A-Za-z0-9-_ ]` (including spaces). # app_path = "My Games/Puzzle Games" ## Add a header image to the app, for displaying in clients. The image must be a @@ -112,6 +116,8 @@ mdns = true ## Enable XWayland support for this application. This is required for any ## applications that are built for the legacy X11 windowing system, such as Steam. +## +## If unset, defaults to `default_app_settings.xwayland`. # xwayland = true ## Force the app to run at 1x. This is useful for applications where you know in @@ -119,18 +125,24 @@ mdns = true ## through XWayland. This setting will ensure that the app always renders at the ## full session resolution, but may result in small font sizes or other UI ## elements. +## +## If unset, defaults to `default_app_settings.force_1x_scale`. # force_1x_scale = false ## Isolate the home directory. If set, the application will see a clean, -## sandboxed $HOME (and /home/$(whoami)), rather than the system-wide one. +## sandboxed `$HOME` (and `/home/$(whoami)`), rather than the system-wide one. ## This home directory is saved between runs of the app to ## `/homes/`. +## +## If unset, defaults to `default_app_settings.isolate_home`. # isolate_home = true ## If `isolate_home` is set to true, this sets a name for the home directory, ## can be shared between apps. For example, multiple apps with this option set ## to 'myhome' will all see the same $HOME when they run. By default, this is ## set to the name of the application. +## +## If unset, defaults to `default_app_settings.shared_home_name`. # shared_home_name = same as application name ## If `isolate_home` is set to true, this mounts a brand new $HOME (using tmpfs) @@ -138,6 +150,8 @@ mdns = true ## ## Note that any data saved while the app is running will be irrevocably ## destroyed when it exits. +## +## If unset, defaults to `default_app_settings.tmp_home`. # tmp_home = false ## ***----------------------***