diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c35f6d..1c479d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb700d1..86a90a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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" diff --git a/AGENTS.md b/AGENTS.md index 10f1dde..ec58322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` diff --git a/README.en.md b/README.en.md index 926bc02..f5bb4d0 100644 --- a/README.en.md +++ b/README.en.md @@ -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--x86_64-linux.tar.gz` (ARM64: `aarch64-linux`) from [Releases](../../releases/latest), extract it, then: +**Other distributions**: download `uu-remote-linux--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--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. diff --git a/README.md b/README.md index 471edb7..f415113 100644 --- a/README.md +++ b/README.md @@ -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 库。系统要求: diff --git a/c_engine/assist_window.c b/c_engine/assist_window.c index 7d3e1fb..e38072c 100644 --- a/c_engine/assist_window.c +++ b/c_engine/assist_window.c @@ -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); @@ -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"); @@ -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); } @@ -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(); } diff --git a/packaging/tarball/install.sh b/packaging/tarball/install.sh new file mode 100755 index 0000000..5f54787 --- /dev/null +++ b/packaging/tarball/install.sh @@ -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 <&2 + exit 1 +fi diff --git a/src/assist.rs b/src/assist.rs index e1e3644..808b1f7 100644 --- a/src/assist.rs +++ b/src/assist.rs @@ -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. diff --git a/src/doctor.rs b/src/doctor.rs index 44c1260..bc52e6f 100644 --- a/src/doctor.rs +++ b/src/doctor.rs @@ -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); diff --git a/src/main.rs b/src/main.rs index 6006ffa..8ef90cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -522,6 +522,7 @@ async fn main() -> Result<(), Box> { let _audio_cleanup=virtual_audio::Cleanup; log::info!("Starting uu-remote-linux host v{}...", env!("CARGO_PKG_VERSION")); video::warm_auto_probe(); + video::check_ffmpeg_usable(); if !desktop_backend::headless() && desktop_backend::detect()? == desktop_backend::DesktopBackend::X11 && desktop_backend::wayland_session_running() { // X11 capture inside a Wayland session reads XWayland's root window: all black, no error (#61). @@ -852,7 +853,7 @@ async fn serve(mut capture: Arc, input: Arc, shutd if desktop { input.set_mac_controller(controller_mac); } log::info!("Offer for connection: capture_type={} desktop={} active_peers={}", capture_type, desktop, sessions.len()); // An expired NVENC failure is re-probed in the background. This offer keeps the cached encoder. - if desktop { video::warm_auto_probe(); } + if desktop { video::warm_auto_probe(); video::check_ffmpeg_usable(); } // Replace only this ICE id or a previous desktop, never tool peers. for id in superseded_peer_ids(&sessions, &ice, desktop) { if let Some(old) = sessions.remove(&id) { let _ = old.pc.close().await; } diff --git a/src/video.rs b/src/video.rs index dc01a1a..00d8f13 100644 --- a/src/video.rs +++ b/src/video.rs @@ -358,6 +358,52 @@ fn ffmpeg_spec() -> FfmpegSpec { pub(crate) fn ffmpeg_command() -> Command { ffmpeg_spec().command() } +/// Encoders of interest that the configured FFmpeg lists; `None` when it cannot run (bounded to 5 s). +pub async fn ffmpeg_encoders() -> Option> { + let mut ffmpeg = ffmpeg_command(); + ffmpeg.args(["-hide_banner", "-encoders"]).kill_on_drop(true); + let output = tokio::time::timeout(Duration::from_secs(5), ffmpeg.output()).await.ok()?.ok()?; + let text = String::from_utf8_lossy(&output.stdout); + Some(["libx264", "libx265", "h264_nvenc", "hevc_nvenc", "h264_vaapi", "hevc_vaapi"].into_iter() + .filter(|name| text.split_whitespace().any(|word| word == *name)).collect()) +} + +/// Without FFmpeg the desktop stream fails after the viewer connects and it waits on +/// 正在传输画面 with no message of its own. Checked at startup and on each desktop offer: +/// logged, shown in the host window, and sent as a desktop notification (at most every 5 minutes). +/// Installing FFmpeg clears the window line on the next check; no restart is needed. +pub fn check_ffmpeg_usable() { + if crate::desktop_backend::headless() { return; } + tokio::spawn(async { + static LAST_NOTICE: std::sync::Mutex> = std::sync::Mutex::new(None); + const SUMMARY: &str = "无法传输远程画面"; + match ffmpeg_encoders().await { + None => { + log::error!("FFmpeg is not runnable: desktop video cannot start and viewers stay on 正在传输画面. Install ffmpeg (Debian/Ubuntu: sudo apt install ffmpeg; Arch: sudo pacman -S ffmpeg; Fedora: ffmpeg from RPM Fusion), or set ffmpeg_root, then check `uuremote-server doctor`"); + let text = "未找到可运行的 FFmpeg,客户端会一直停在「正在传输画面」。\n请安装后重新连接:sudo apt install ffmpeg(Arch:sudo pacman -S ffmpeg;Fedora 需先启用 RPM Fusion)"; + let previous = { + let mut last = LAST_NOTICE.lock().unwrap_or_else(|e| e.into_inner()); + let previous = *last; + let due = previous.is_none_or(|at| at.elapsed() >= Duration::from_secs(300)); + if due { *last = Some(Instant::now()); } + due.then_some(previous) + }; + // The notification is a blocking D-Bus call (bounded to 3 s). + let told = tokio::task::spawn_blocking(move || crate::assist::show_problem(SUMMARY, Some(text), previous.is_some())).await.unwrap_or(false); + // Only a notice that reached the person starts the 5-minute quiet period. + if let (Some(previous), false) = (previous, told) { + *LAST_NOTICE.lock().unwrap_or_else(|e| e.into_inner()) = previous; + log::warn!("Missing FFmpeg: no desktop notification daemon and no host window to show it"); + } + } + Some(found) => { + if !found.contains(&"libx264") { log::warn!("FFmpeg has no libx264: CPU encoding and the fallback after a hardware encoder failure are unavailable. Install a full ffmpeg build"); } + tokio::task::spawn_blocking(|| crate::assist::show_problem(SUMMARY, None, false)); + } + } + }); +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FfmpegTimingMode { Modern, Legacy }