niri 下腾讯会议共享黑屏:从 XShmGetImage 到 PipeWire Hook

niri(Wayland)下腾讯会议共享全黑,问题不在 portal 配置,而在 Arch wemeet-bin 的捕获库只会用 X11 XShmGetImage。本文从 xcast 日志到 ldd/strings 一步步定位,并给出 LD_PRELOAD hook 的修复办法。

在 niri(Wayland)下打开腾讯会议,进会议点击共享,自己共享出去是全黑,但音频正常。本文记录从日志到原因的完整排查,并给出针对 Arch wemeet-bin 的修复办法。先说结论:问题不在 portal 配置,而在于这个官方二进制的屏幕捕获只会用 X11 截屏。

起因是 linux.do 上的一篇经验帖:在 niri 下实现腾讯会议屏幕共享。那篇基于 NixOS,照着做在 Arch 上没成功,于是有了这篇排查记录。

现象与环境

会议里点击共享后,对端和本地预览都是一片黑。终端能看到 wemeet 在持续刷类似下面的日志:

MicButtonQTView::OnViewStatefulChanged():btn_title:选择音频
Error in received packet: 没有那个文件或目录

环境:

项目
合成器 niri 26.04(纯 Wayland,wlroots 系)
会话 XDG_SESSION_TYPE=wayland,Xwayland 以 rootless 模式运行
显卡 NVIDIA RTX 4070 Mobile + AMD 核显
wemeet AUR wemeet-bin 3.26.10.401(打包腾讯官方二进制)
音视频栈 PipeWire 1.6.6 + WirePlumber 0.5.14
门户 xdg-desktop-portal-gnome / gtk / hyprland 均已安装

这里有一点容易让人判断错方向:门户链路本身是正常的。busctl --user list 能看到 niri 进程注册了 org.gnome.Mutter.ScreenCastxdg-desktop-portal-gnome 也在运行并对外提供 ScreenCast 接口。换句话说,一个按规范走 portal 申请屏幕捕获的程序,本该能拿到画面。问题在于 wemeet 没有走这条路。

第一步:读共享流的日志

wemeet 把屏幕共享相关日志写在 ~/.local/share/wemeetapp/Saas/Logs/xcast_*.log。屏幕共享是其中名为 sub0 的视频子流。捕获画面期间,日志反复出现:

'sub0' CAPSTAT last frame=NULL elapse:1338311
...
Snd[sub Cap:0x0@0.0/0.0@0/1/0 HW:0 ...]
capfps is 0, src 2

last frame=NULLCap:0x0capfps is 0 这三条放在一起,意思已经比较明确:捕获模块拿到的画面是空的,分辨率 0×0,帧率 0。同一时间音频流(Aud)还在正常发包。也就是说,会议连接、编码、网络都没问题,问题就出在屏幕画面捕获这一环。

第二步:看 wemeet 到底用什么方式截屏

直接检查捕获库 libxcast.so 链接了什么、用了什么 API:

ldd /usr/lib/wemeet/libxcast.so | grep -iE 'pipewire|wayland|X11|xcb|EGL'
#   libX11.so.6
#   libEGL.so.1
#   libxcb.so.1

strings /usr/lib/wemeet/libxcast.so | grep -iE 'XShmGetImage|XGetImage|pipewire|ScreenCast'
#   XGetImage
#   XShmGetImage

结果很清楚:捕获库只链接 libX11/libxcb/libEGL,内部用的是 XShmGetImage/XGetImage,既没有链接 libpipewire,也不含任何 portal/ScreenCast 字符串。把整个安装目录都扫一遍,结论一样:

grep -rilE 'pipewire|org.freedesktop.portal|ScreenCast' /opt/wemeet/ /usr/lib/wemeet/
# (无任何输出)

那些 libQt5WaylandClient.soplugins/wayland-* 是 Qt 画窗口用的渲染插件,与屏幕捕获无关,不要被名字误导。

第三步:为什么 X11 截屏在 niri 下会黑

读 AUR 包的启动脚本 /usr/bin/wemeet,关键在这一段:

elif [ "$XDG_SESSION_TYPE" = 'wayland' ]; then
    export QT_QPA_PLATFORM=xcb
    export XDG_SESSION_TYPE=x11
    unset WAYLAND_DISPLAY
    export WEMEET_XWAYLAND=1
fi

它在 Wayland 会话下,强制把 wemeet 降级成 X11 程序运行。验证运行中进程的环境,确实生效:

tr '\0' '\n' < /proc/$(pgrep -x wemeetapp)/environ | grep -E 'XDG_SESSION_TYPE|WEMEET_XWAYLAND'
# XDG_SESSION_TYPE=x11
# WEMEET_XWAYLAND=1

于是 wemeet 用 XShmGetImage 去抓 X11 的 root window。但 niri 的 Xwayland 跑在 rootless 模式(进程命令行里能看到 Xwayland :0 ... -rootless)。rootless 模式下,X11 的 root window 不包含任何真实桌面内容,niri 渲染的原生 Wayland 窗口对 X11 也就完全不可见,抓到的画面自然是全黑。

把整条因果关系串起来是这样:

X11 XShmGetImage 截屏(libxcast.so,无 pipewire)
  └─ 启动脚本强制 XDG_SESSION_TYPE=x11 + rootless Xwayland
       └─ X11 root window 不含 Wayland 窗口画面
            └─ 抓到全黑 → 日志 CAPSTAT last frame=NULL, Cap:0x0

为什么照搬社区帖子的方案无效

社区里关于「niri 跑腾讯会议」的帖子,大多基于 NixOS,比如开头提到的那篇 linux.do 经验帖,它又参考了更早的 niri 如何正确运行腾讯会议。NixOS 的 wemeet 包在外面套了一层 PipeWire 时代的 screenshare hook(由打包方注入),所以那边的三步方案(给 niri 打 shm 补丁、把 ScreenCast portal 指向某后端、wemeet 走 XWayland)能用。

逐条对照 Arch 环境会发现,这些步骤要么早就满足了,要么在 Arch 包上根本不起作用:

帖子步骤 Arch + niri 26.04 实际情况
给 niri 打 shm 补丁 PR #1791 26.04 已并入主线,无需打
ScreenCast portal 指向后端 配了也没效果,因为 wemeet-bin 根本不调用 portal
wemeet 走 XWayland 启动脚本已经自动强制,不用手动配
Mesa EGL 优先 NVIDIA 上可能需要,但不是黑屏的主要原因

真正起作用的那一环——把 portal/PipeWire 的画面交给 wemeet 的 hook——Arch 的官方 wemeet-bin 里没有。缺的就是这一块。

需要澄清一个常见误解:wemeet-bin 已经是 AUR 上的最新版,打包的就是腾讯官方二进制(PKGBUILD 从 updatecdn.meeting.qq.com 直接下载 deb)。但「版本最新」解决不了黑屏,因为这个官方二进制本身就没写 Wayland 捕获路径,再新的版本在 niri 下结果一样。

解决方案:LD_PRELOAD hook 拦截 X11 截屏

社区项目 wemeet-wayland-screenshare(作者 xuwd1)正好针对「X11 截屏 + Wayland 合成器」这种组合:它用 LD_PRELOAD 注入一个 libhook.so,拦截 wemeet 的 XShmGetImage 调用,改从 portal/PipeWire 取真实画面再交回给 wemeet。验证一下它的 libhook.so

ldd libhook.so | grep -iE 'portal|pipewire'
#   libportal.so.1
#   libpipewire-0.3.so.0

strings libhook.so | grep -iE 'xdp_portal_create_screencast_session|XShmGetImage'
#   xdp_portal_create_screencast_session
#   XShmGetImageHook

它的做法和前面排查出的原因正好对得上。需要说明的是,这个项目已经在 2025 年归档(README 里提到官方某些渠道的包已经原生支持 Wayland),但对 Arch wemeet-bin 这条路来说,它目前仍是唯一对症的方案。hook 是为 wemeet 3.19.2 时期写的,拦截的是通用的 X11 调用,跨版本一般还能用,但要自己实测确认。

安装步骤(Arch / CachyOS)

依赖(多数桌面环境本就装了):

sudo pacman -S --needed wireplumber libportal xdg-desktop-portal qt5-wayland opencv libxrandr

安装 hook 包:

paru -S wemeet-wayland-screenshare-git
# 或本地构建:
# git clone --recursive https://github.com/xuwd1/wemeet-wayland-screenshare.git
# cd wemeet-wayland-screenshare && mkdir build && cd build
# cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release && ninja && sudo ninja install

它装好后提供一个启动器 /usr/bin/wemeet-wayland-screenshare,内容很简单:

#!/bin/bash
export LD_PRELOAD="/usr/lib/wemeet/libhook.so"
exec /usr/bin/wemeet-x11 "$@"

注意它走的是 wemeet-x11(即强制 X11 模式),hook 正是要配合这个模式工作,二者不冲突。

确认 portal 配置

hook 通过 portal 取画面,所以 portal 后端配置这时才真正生效。检查 ~/.config/xdg-desktop-portal/ 下的配置,接口名必须是大小写正确的 ScreenCast(不是 Screencast,写错会被静默忽略):

[preferred]
default=gnome;gtk
org.freedesktop.impl.portal.ScreenCast=gnome
org.freedesktop.impl.portal.RemoteDesktop=gnome

改完重启 portal:

systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gnome.service

启动与验证

用带 hook 的启动器打开:

wemeet-wayland-screenshare

确认 hook 已注入主进程:

grep -l libhook /proc/$(pgrep -x wemeetapp)/maps
# /proc/<pid>/maps  ← 有输出即已注入

进会议点共享,正常情况下会弹出一个 GNOME 样式的 portal 屏幕选择窗口,这说明 hook 已经在工作了。选屏幕后开始共享,自己的预览应该有画面、不再是黑的。可能会跟着冒出几个绿色小窗口,是已知的无害小 bug,不影响共享。

成功后再看 xcast 日志,之前的 last frame=NULL 会消失,取而代之的是正常的捕获帧。

小结

  • 黑屏的原因是 Arch wemeet-bin 的捕获库 libxcast.so 只会用 X11 的 XShmGetImage,在 niri 的 rootless Xwayland 下抓不到任何 Wayland 窗口画面。
  • 门户配置、wemeet 版本新旧都不是主要原因,官方二进制根本不调用 portal。
  • 修复办法是用 LD_PRELOAD 注入 libhook.so,拦截 X11 截屏,改从 PipeWire 取画面。
  • 排查思路上,用 lddstrings 看一个二进制到底用什么方式截屏,比反复改配置试错要快不少。
Interaction

读完之后

分享海报
Interaction

评论区