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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ jobs:
uuremote-server --help | head -1
test -f /usr/lib/uu-remote-linux/libuu-wayland.so
systemd-analyze verify /usr/lib/systemd/user/uuremote-native.service
# The tarball installer must bring FFmpeg back on a machine without it (no FFmpeg = no picture).
- name: Tarball install script
run: |
shellcheck packaging/tarball/install.sh
sudo apt-get remove -y ffmpeg || true
if command -v ffmpeg; then echo "ffmpeg still present"; exit 1; fi
pkg=$(mktemp -d)
cp target/debug/uuremote-server target/debug/libuu-wayland.so packaging/tarball/install.sh "$pkg/"
cp -r contrib "$pkg/"
home=$(mktemp -d)
HOME=$home XDG_CONFIG_HOME=$home/.config "$pkg/install.sh" --yes
ffmpeg -hide_banner -encoders | grep -qw libx264
test -x "$home/.local/bin/uuremote-server" && test -f "$home/.local/lib/uu-remote-linux/libuu-wayland.so"
grep -q '^ExecStart=%h/.local/bin/uuremote-server$' "$home/.config/systemd/user/uuremote-native.service"
HOME=$home XDG_CONFIG_HOME=$home/.config "$pkg/install.sh" --yes | grep -q 'Kept existing'

test-viewer:
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
name=uu-remote-linux-${GITHUB_REF_NAME}-${{ matrix.arch }}-linux
mkdir -p "$name"
cp target/release/uuremote-server target/release/uuremote-viewer target/release/libuu-wayland.so target/release/libuu-portal.so \
LICENSE README.md README.en.md config.example.json "$name/"
packaging/tarball/install.sh LICENSE README.md README.en.md config.example.json "$name/"
cp -r contrib "$name/"
tar czf "$name.tar.gz" "$name"
sha256sum "$name.tar.gz" > "$name.tar.gz.sha256"
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ cargo build --release --features native-viewer,viewer-sdl --bin uuremote-viewer
X11 → default build. Hyprland → `--features wayland`. GNOME/KDE on Wayland → `--features wayland,portal`
(or advise the user to pick the "on Xorg" session at the login screen, which is the best-supported path).
2. Install dependencies and build (section 3). Copy the binary: `install -Dm755 target/release/uuremote-server ~/.local/bin/uuremote-server`.
From a release tarball run its `./install.sh` as the desktop user instead: it installs into `~/.local`, writes the user unit
(keeps an existing one) and installs FFmpeg with libx264 via sudo apt/pacman when missing (Fedora/openSUSE: prints the command).
The .deb depends on `ffmpeg`. Without FFmpeg the viewer waits on 正在传输画面; the host checks at startup and on each desktop offer, logs an error and tells the desktop user (freedesktop notification, else opens its window with a red line; `assist::show_problem`).
3. **Login (needs the human).** Run `uuremote-server login` inside the graphical session. A GTK window shows a QR
code; the user scans it with the UU Remote mobile app and confirms. Without a display use
`uuremote-server login --no-gui` (terminal QR). Credentials are written to `~/.config/uuremote/config.json`
Expand Down
13 changes: 9 additions & 4 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,21 @@ uuremote-server login # scan the QR code with
systemctl --user enable --now uuremote-native.service # start with the graphical session
```

The deb installs the binaries, plugins, a systemd user unit and menu entries; uinput access stays opt-in (step 5).
The deb installs the binaries, plugins, a systemd user unit and menu entries, and pulls in FFmpeg as a dependency; uinput access stays opt-in (step 5).
GNOME and KDE pass the display environment to the unit; on desktops that don't (e.g. Xfce) add `DISPLAY` as in step 4.

**Other distributions**: download `uu-remote-linux-<version>-x86_64-linux.tar.gz` (ARM64: `aarch64-linux`) from [Releases](../../releases/latest), extract it, then:
**Other distributions**: download `uu-remote-linux-<version>-x86_64-linux.tar.gz` (ARM64: `aarch64-linux`) from [Releases](../../releases/latest), extract it, then run as your desktop user (not root):

```sh
install -Dm755 uuremote-server ~/.local/bin/uuremote-server
install -Dm644 libuu-wayland.so libuu-portal.so -t ~/.local/lib/uu-remote-linux/
cd uu-remote-linux-<version>-x86_64-linux
./install.sh # --yes: don't ask; --no-ffmpeg: check only
```

The script installs the binaries and plugins under `~/.local`, writes the systemd user unit (an existing one is kept) and checks
FFmpeg with libx264 (without it the viewer stays on 正在传输画面). A missing FFmpeg is installed through sudo with apt or pacman;
Fedora needs RPM Fusion and openSUSE needs Packman first, and the script only prints the command instead of adding third-party repositories.
The host also checks FFmpeg at startup and on every desktop connection: when it is not runnable it logs an error and shows a desktop notification (without a notification daemon it opens its window with a red hint), at most every 5 minutes. Install it and reconnect; no restart is needed.

One package covers X11, wlroots compositors (Hyprland, Sway, niri) and GNOME/KDE Wayland: the backend is chosen from the
running session at startup, and the matching `libuu-*.so` is loaded only on Wayland, so X11 hosts need no Wayland libraries.

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,20 @@ uuremote-server login # 用 UU 远程 App 扫
systemctl --user enable --now uuremote-native.service # 随图形会话启动
```

deb 会安装程序、插件、用户级 systemd 服务和应用菜单图标;uinput 权限不会自动开启(见第 5 步)。
deb 会安装程序、插件、用户级 systemd 服务和应用菜单图标,并通过依赖自动装上 FFmpeg;uinput 权限不会自动开启(见第 5 步)。
GNOME / KDE 会把显示环境自动传给服务;Xfce 等不传的桌面需按第 4 步补 `DISPLAY`。

**其他发行版**:从 [Releases](../../releases/latest) 下载 `uu-remote-linux-<版本>-x86_64-linux.tar.gz`(ARM64 选 `aarch64-linux`),解压后:
**其他发行版**:从 [Releases](../../releases/latest) 下载 `uu-remote-linux-<版本>-x86_64-linux.tar.gz`(ARM64 选 `aarch64-linux`),解压后用普通桌面用户(不要 root)运行:

```sh
install -Dm755 uuremote-server ~/.local/bin/uuremote-server
install -Dm644 libuu-wayland.so libuu-portal.so -t ~/.local/lib/uu-remote-linux/
cd uu-remote-linux-<版本>-x86_64-linux
./install.sh # 加 --yes 不询问;--no-ffmpeg 只检查不安装
```

脚本把程序和插件装到 `~/.local`,写入用户级 systemd 服务(已有则保留),并检查 FFmpeg(需带 libx264,否则客户端会一直停在「正在传输画面」)。
缺 FFmpeg 时经 sudo 用 apt / pacman 安装;Fedora 需先启用 RPM Fusion,openSUSE 需 Packman 源,脚本只提示命令、不替你添加第三方源。
服务启动时和每次有人连接桌面时都会检查 FFmpeg:不可用就记 error,并弹桌面通知(没有通知服务时打开主窗口,顶部红字提示安装方法,最多每 5 分钟一次);装好后重新连接即可,不用重启服务。

同一个包支持 X11、Hyprland 等 wlroots 合成器和 GNOME/KDE Wayland:程序启动时按当前会话自动选择后端,只在 Wayland 会话里
才加载对应的 `libuu-*.so`,X11 用户不需要任何 Wayland 库。系统要求:

Expand Down
57 changes: 57 additions & 0 deletions c_engine/assist_window.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ static int suppress_tray;
static int cur_verify, cur_refresh, cur_has_custom;
static char current_id[64], current_code[64];
static int code_visible = 1, suppress_switch, start_hidden, close_hides;
// A problem only the person at the desktop can fix (e.g. FFmpeg missing): red line at the top.
static GtkWidget *problem_label;
static gchar *problem_text;
static pthread_mutex_t problem_lock = PTHREAD_MUTEX_INITIALIZER;

static GtkWidget *label(const char *text, const char *cls) {
GtkWidget *l = gtk_label_new(text);
Expand Down Expand Up @@ -399,6 +403,11 @@ static void build_window(void) {
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_container_set_border_width(GTK_CONTAINER(root), 24);
gtk_container_add(GTK_CONTAINER(window), root);
problem_label = label("", "status");
gtk_style_context_add_class(gtk_widget_get_style_context(problem_label), "error");
gtk_label_set_line_wrap(GTK_LABEL(problem_label), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(problem_label), 40);
gtk_box_pack_start(GTK_BOX(root), problem_label, FALSE, FALSE, 0);

GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_style_context_add_class(gtk_widget_get_style_context(card), "card");
Expand Down Expand Up @@ -509,6 +518,10 @@ static void build_window(void) {
gtk_widget_set_visible(assist_card, show_assist);
gtk_widget_set_visible(hint_label, show_assist);
gtk_widget_set_visible(account_card, show_account);
pthread_mutex_lock(&problem_lock);
gtk_label_set_text(GTK_LABEL(problem_label), problem_text ? problem_text : "");
gtk_widget_set_visible(problem_label, problem_text != NULL);
pthread_mutex_unlock(&problem_lock);
if (!start_hidden) gtk_widget_show(window);
}

Expand Down Expand Up @@ -691,6 +704,50 @@ void uu_login_ui_offer_refresh(int visible) {
g_idle_add(apply_qr_refresh, GINT_TO_POINTER(visible != 0));
}
int uu_login_ui_take_refresh(void) { return atomic_exchange(&qr_refresh_requested, 0); }

static gboolean apply_problem(gpointer data) {
(void)data;
if (window && problem_label) {
pthread_mutex_lock(&problem_lock);
gtk_label_set_text(GTK_LABEL(problem_label), problem_text ? problem_text : "");
gtk_widget_set_visible(problem_label, problem_text != NULL);
pthread_mutex_unlock(&problem_lock);
}
return G_SOURCE_REMOVE;
}
// Freedesktop notification through the session bus (no GTK main loop needed). Returns 0 when delivered.
static int notify_desktop(const char *summary, const char *body) {
GDBusConnection *bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
if (!bus) return -1;
GVariantBuilder actions, hints;
g_variant_builder_init(&actions, G_VARIANT_TYPE("as"));
g_variant_builder_init(&hints, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&hints, "{sv}", "urgency", g_variant_new_byte(2)); // critical: stays until dismissed
GVariant *reply = g_dbus_connection_call_sync(bus, "org.freedesktop.Notifications", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", "Notify",
g_variant_new("(susssasa{sv}i)", "UU 远程 Linux 主机", 0u, "uu-remote-linux", summary, body, &actions, &hints, -1),
G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE, 3000, NULL, NULL);
g_object_unref(bus);
if (!reply) return -1;
g_variant_unref(reply);
return 0;
}
// text NULL clears the window line. notify: also tell the person now, by a desktop notification or,
// without a notification daemon (e.g. bare Xfce), by opening the host window that shows the line.
// Returns 0 notified, 1 window opened, -1 neither (no notify requested, or no daemon and no window).
int uu_ui_set_problem(const char *summary, const char *text, int notify) {
pthread_mutex_lock(&problem_lock);
g_free(problem_text);
problem_text = text ? g_strdup(text) : NULL;
pthread_mutex_unlock(&problem_lock);
int window_ready = ui_started && init_state == 1 && !atomic_load(&closed);
if (window_ready) g_idle_add(apply_problem, NULL);
if (!notify || !text) return -1;
if (notify_desktop(summary ? summary : "", text) == 0) return 0;
if (!window_ready) return -1;
uu_assist_ui_present();
return 1;
}
void uu_login_ui_set_done(const char *text) { uu_ui_set_user(text); }
int uu_login_ui_closed(void) { return atomic_load(&closed); }
void uu_login_ui_close(void) { uu_assist_ui_close(); }
131 changes: 131 additions & 0 deletions packaging/tarball/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/bin/sh
# Install uu-remote-linux from the release tarball for the current desktop user, and install FFmpeg
# (needed for desktop video) through the system package manager when it is missing.
# usage: ./install.sh [--yes] [--no-ffmpeg]
# --yes install FFmpeg without asking
# --no-ffmpeg only check FFmpeg, never install it
set -eu

assume_yes=0 ffmpeg_install=1
for arg in "$@"; do
case $arg in
-y|--yes) assume_yes=1 ;;
--no-ffmpeg) ffmpeg_install=0 ;;
-h|--help) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $arg (see --help)" >&2; exit 2 ;;
esac
done

if [ "$(id -u)" = 0 ]; then
echo "Run this as your desktop user, not root: the host is a per-user service. It asks for sudo only to install FFmpeg." >&2
exit 1
fi

src=$(cd "$(dirname "$0")" && pwd)
bin=$HOME/.local/bin lib=$HOME/.local/lib/uu-remote-linux share=$HOME/.local/share
unit_dir=${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user

# A running binary cannot be overwritten in place ("text file busy"): copy, then rename.
put() { install -Dm"$1" "$2" "$3.new" && mv -f "$3.new" "$3"; }

[ -f "$src/uuremote-server" ] || { echo "uuremote-server not found next to install.sh" >&2; exit 1; }
put 755 "$src/uuremote-server" "$bin/uuremote-server"
[ -f "$src/uuremote-viewer" ] && put 755 "$src/uuremote-viewer" "$bin/uuremote-viewer"
for so in libuu-wayland.so libuu-portal.so; do
[ -f "$src/$so" ] && put 644 "$src/$so" "$lib/$so"
done
if [ -d "$src/contrib/desktop" ]; then
put 644 "$src/contrib/desktop/uu-remote-linux.svg" "$share/icons/hicolor/scalable/apps/uu-remote-linux.svg"
put 644 "$src/contrib/desktop/uu-remote-linux.desktop" "$share/applications/uu-remote-linux.desktop"
[ -f "$bin/uuremote-viewer" ] && put 644 "$src/contrib/desktop/uu-remote-linux-viewer.desktop" "$share/applications/uu-remote-linux-viewer.desktop"
fi
echo "Installed uuremote-server to $bin"

# Same unit as the .deb, pointing at ~/.local/bin. An existing unit is the user's: leave it alone.
if [ -f "$unit_dir/uuremote-native.service" ]; then
echo "Kept existing $unit_dir/uuremote-native.service"
else
mkdir -p "$unit_dir"
cat > "$unit_dir/uuremote-native.service" <<'UNIT'
[Unit]
Description=UU Remote Linux host (unofficial)
After=graphical-session.target
PartOf=graphical-session.target

[Service]
Environment=RUST_LOG=info
ExecStart=%h/.local/bin/uuremote-server
Restart=always
RestartSec=5

[Install]
WantedBy=graphical-session.target
UNIT
echo "Created $unit_dir/uuremote-native.service"
fi
if command -v systemctl >/dev/null 2>&1; then systemctl --user daemon-reload 2>/dev/null || true; fi

# FFmpeg with libx264 is required: without it the viewer waits on 正在传输画面 forever.
ffmpeg_ok() { command -v ffmpeg >/dev/null 2>&1 && ffmpeg -hide_banner -encoders 2>/dev/null | grep -qw libx264; }

ffmpeg_cmd=""
ffmpeg_note=""
if command -v apt-get >/dev/null 2>&1; then
ffmpeg_cmd="apt-get update && apt-get install -y ffmpeg"
elif command -v pacman >/dev/null 2>&1; then
ffmpeg_cmd="pacman -S --needed --noconfirm ffmpeg"
elif command -v dnf >/dev/null 2>&1; then
# Fedora's own ffmpeg-free has no libx264; the full build is in RPM Fusion, which we do not enable for you.
if dnf repolist --enabled 2>/dev/null | grep -qi rpmfusion-free; then
ffmpeg_cmd="dnf install -y --allowerasing ffmpeg"
else
ffmpeg_note="Enable RPM Fusion (https://rpmfusion.org/Configuration), then: sudo dnf install --allowerasing ffmpeg"
fi
elif command -v zypper >/dev/null 2>&1; then
ffmpeg_note="openSUSE's ffmpeg has no libx264; add the Packman repository, then: sudo zypper install ffmpeg"
else
ffmpeg_note="Install FFmpeg with libx264 from your distribution, or set \"ffmpeg_root\" in ~/.config/uuremote/config.json"
fi

ffmpeg_ready=1
if ffmpeg_ok; then
echo "FFmpeg with libx264: found"
else
if command -v ffmpeg >/dev/null 2>&1; then echo "FFmpeg is installed but has no libx264."; else echo "FFmpeg is not installed."; fi
if [ "$ffmpeg_install" = 1 ] && [ -n "$ffmpeg_cmd" ]; then
answer=n
if [ "$assume_yes" = 1 ]; then
answer=y
elif [ -t 0 ]; then
printf 'Install it now with: sudo sh -c "%s"? [Y/n] ' "$ffmpeg_cmd"
read -r answer || answer=n
case $answer in ""|y|Y|yes|YES) answer=y ;; esac
fi
if [ "$answer" = y ]; then
if [ "$(id -u)" != 0 ] && ! command -v sudo >/dev/null 2>&1; then
echo "sudo is not available; run as root: $ffmpeg_cmd" >&2
else
sudo sh -c "$ffmpeg_cmd" || echo "FFmpeg installation failed." >&2
fi
fi
fi
if ffmpeg_ok; then
echo "FFmpeg with libx264: installed"
else
ffmpeg_ready=0
if [ -n "$ffmpeg_cmd" ]; then echo "Install it with: sudo sh -c \"$ffmpeg_cmd\""; else echo "$ffmpeg_note"; fi
fi
fi

case ":$PATH:" in *":$bin:"*) ;; *) echo "Note: $bin is not on PATH; add it or call $bin/uuremote-server directly." ;; esac
cat <<NEXT

Next steps (as this user, inside the graphical session):
uuremote-server login # scan the QR code with the UU Remote app
systemctl --user enable --now uuremote-native.service # start with the graphical session
Desktops that do not export DISPLAY to systemd (e.g. Xfce) need a drop-in; see README step 4.
NEXT
if [ "$ffmpeg_ready" = 0 ]; then
echo "WARNING: desktop video will not start until FFmpeg with libx264 is installed." >&2
exit 1
fi
14 changes: 14 additions & 0 deletions src/assist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ extern "C" {
fn uu_assist_ui_set(id: *const libc::c_char, code: *const libc::c_char, allowed: libc::c_int, verify: libc::c_int, refresh: libc::c_int, has_custom: libc::c_int);
fn uu_assist_ui_take_command(arg: *mut libc::c_char, len: libc::size_t) -> libc::c_int;
fn uu_assist_ui_close();
fn uu_ui_set_problem(summary: *const libc::c_char, text: *const libc::c_char, notify: libc::c_int) -> libc::c_int;
}

/// A problem only the person at the desktop can fix: a red line in the host window (`None` clears it)
/// and, with `notify`, a desktop notification, or the host window opened when no notification daemon
/// answers. Returns whether the person was told now.
pub fn show_problem(summary: &str, text: Option<&str>, notify: bool) -> bool {
#[cfg(uu_gtk)] {
let summary = std::ffi::CString::new(summary).unwrap_or_default();
let text = text.map(|t| std::ffi::CString::new(t).unwrap_or_default());
let text = text.as_ref().map_or(std::ptr::null(), |t| t.as_ptr());
unsafe { uu_ui_set_problem(summary.as_ptr(), text, i32::from(notify)) >= 0 }
}
#[cfg(not(uu_gtk))] { let _ = (summary, text, notify); false }
}

/// "243829726" -> "243 829 726", as the official client shows device IDs.
Expand Down
13 changes: 3 additions & 10 deletions src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,9 @@ pub async fn run() -> Result<(), String> {
// Both codecs offered: hardware prefers H.265, CPU stays H.264 unless the preference forces one.
let resolved = crate::capture_setting::pick_codec(true, true, crate::video_select::codec_force(selection.codec_pref), crate::video_select::hardware(selection.encoder));
line("resolved codec", format!("{resolved:?}"));
let mut ffmpeg = crate::video::ffmpeg_command();
ffmpeg.args(["-hide_banner", "-encoders"]).kill_on_drop(true);
match tokio::time::timeout(std::time::Duration::from_secs(5), ffmpeg.output()).await {
Ok(Ok(output)) => {
let text = String::from_utf8_lossy(&output.stdout);
let found: Vec<&str> = ["libx264", "libx265", "h264_nvenc", "hevc_nvenc", "h264_vaapi", "hevc_vaapi"].into_iter()
.filter(|name| text.split_whitespace().any(|word| word == *name)).collect();
line("ffmpeg encoders", if found.is_empty() { "none of libx264/libx265/nvenc/vaapi".to_owned() } else { found.join(", ") });
}
_ => line("ffmpeg encoders", "ffmpeg not runnable (install ffmpeg or set ffmpeg_root)"),
match crate::video::ffmpeg_encoders().await {
Some(found) => line("ffmpeg encoders", if found.is_empty() { "none of libx264/libx265/nvenc/vaapi".to_owned() } else { found.join(", ") }),
None => line("ffmpeg encoders", "ffmpeg not runnable (install ffmpeg or set ffmpeg_root)"),
}
let mut version = crate::video::ffmpeg_command();
version.arg("-version").kill_on_drop(true);
Expand Down
Loading
Loading