wayland抓屏

9 阅读8分钟

概述
基于项目需要,原来的程序运行在x11桌面环境,当程序运行在wayland协议的桌面环境时,之前的抓图方法就不行了,典型的wayland桌面环境有ubuntu22、ubunt24等。

要求
一次编译,多平台运行。

方案
wayland抓屏有通用的标准方式“XDG”,“XDG”是一个通用的门户接口,各个服务通过dbus接口调用该服务,由该服务提供统一的服务,其中的“抓屏”就是该门户接口的一个子功能接口。

"org.freedesktop.portal.Desktop", 
"/org/freedesktop/portal/desktop", 
"org.freedesktop.portal.Screenshot", 
"Screenshot",

注意wayland下会有弹窗。

直接调用dbus接口
直接通过dbus接口调用这个抓屏服务非常繁琐,容易出差,我是没成功过,且会随着平台变化,dbus版本变化,调用接口方式也会变化,所以直接调用不可行。
当然直接调用也有好处,就是依赖少,直接通过系统dbus调用服务。
libporta
通过libportal封装的接口调用dbus,该库是对dbus调用接口的封装。
libportal:github.com/flatpak/lib…
libportal0.5:github.com/flatpak/lib…

实现

  1. 通过libporta调用dbus接口,进而调用"org.freedesktop.portal.Desktop"服务,进行屏幕抓屏;
  2. 为了实现一个平台编译多平台运行,选择uos的x11编译,实现在x11、wayland平台的运行;
  3. uos上编译因为缺少libportal的预编译版本,所以自己编译。
  4. 当然,要实现一次编译多出运行,必须识别桌面协议类型,是x11就走x11那套逻辑,是wayland就走wayland那套逻辑;一次编译只是说一次编译,运行还是要根据实际环境走不同的代码路径的。

libportal编译方式

# 在uos上编译libportal-0.5(最高也就只能编译到这个版本了,搞了编译不了,低了没法用)
# 下载地址: https://github.com/flatpak/libportal/tree/main

# 1.安装必要的依赖:
	sudo apt install meson ninja-build g++ pkg-config
	sudo apt install libgirepository1.0-dev gobject-introspection
	sudo apt install gtk-doc-tools
	sudo apt install libgstreamer-plugins-base1.0-dev
        
# 2.编译
# -Dbackends=gtk3:
#    因为系统安装的是gtk3,没有gtk4,而libportal0.5默认是基于gtk4编译的,  
#    所以无法编译过,需要关闭对gtk4编译的的支持,只编译gtk3,
# -Dvapi=false  
#    一个无用的依赖,直接关掉,否则还要安装对应的依赖。
	rm build build/ -rf
	meson setup build --prefix=/home/uos/xxx/code/libportal/libportal-0.5/install  -Dbackends=gtk3 -Dvapi=false -Ddocs=false
	ninja -C build
	ninja -C build install

# 3.编译成功后安装文件在:
	/home/uos/xxx/code/libportal/libportal-0.5/install

有了编译的libportal,就可以基于此编译wayland的截屏程序了。
注意:uos上虽然编译过了,但是不能运行,运行还是必须在wayland协议的ubuntu22或者更高版本上运行,但是也达到了一次编译,多个发行版本运行的目的。

编译时注意libportal-0.5位置替换为本地实际位置。

测试程序(screencast_portal_png.cpp)抓图保存一个png,只适合抓一个图片的场景。 如果要抓连续的帧就必须采用后面2个程序例子了

/*
 * 本地 libportal-0.5 最终可编译版
 * g++ screencast_portal_png.cpp   \
    -I/home/uos/xxx/code/libportal/libportal-0.5/install/include   \
    -L/home/uos/xxx/code/libportal/libportal-0.5/install/lib/x86_64-linux-gnu   \
    `pkg-config --cflags --libs gtk+-3.0 gstreamer-1.0` \
    -lportal -o portal_png
 */
#include <libportal/portal.h>
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <unistd.h>

static GMainLoop *g_loop = nullptr;
static int        g_pw_fd = -1;

static void on_start_done(GObject *, GAsyncResult *, gpointer);
static void on_create_done(GObject *, GAsyncResult *, gpointer);

/* 1. 创建会话(参数顺序:portal,output,flags,cursor,persist,parent,cancellable,callback,userdata) */
static void create_session(XdpPortal *p)
{
    xdp_portal_create_screencast_session(
        p,
        XDP_OUTPUT_MONITOR,
        XDP_SCREENCAST_FLAG_NONE,
        XDP_CURSOR_MODE_EMBEDDED,
        XDP_PERSIST_MODE_NONE,
        nullptr,        /* parent handle */
        nullptr,        /* cancellable   */
        on_create_done, /* callback      */
        p);             /* userdata      */
}

/* 2. 会话创建成功 → start */
static void on_create_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpPortal *portal = XDP_PORTAL(src);
    XdpSession *session =
        xdp_portal_create_screencast_session_finish(portal, res, &err);
    if (!session) {
        g_warning("创建会话失败: %s", err->message);
        g_main_loop_quit(g_loop);
        return;
    }
    xdp_session_start(session, nullptr, nullptr, on_start_done, session);
}

/* 3. start 完成 → 拿 fd 并播放 */
static void on_start_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpSession *session = XDP_SESSION(src);
    if (!xdp_session_start_finish(session, res, &err)) {
        g_warning("Start 失败: %s", err->message);
        g_main_loop_quit(g_loop);
        return;
    }

    g_pw_fd = xdp_session_open_pipewire_remote(session);
    if (g_pw_fd < 0) {
        g_warning("拿不到 PipeWire fd");
        g_main_loop_quit(g_loop);
        return;
    }

    GVariant *streams = xdp_session_get_streams(session);
    guint node_id = 0;
    if (streams && g_variant_n_children(streams) > 0) {
        guint32 id; GVariant *props;
        g_variant_get_child(streams, 0, "(u@a{sv})", &id, &props);
        node_id = id; g_variant_unref(props);
    }
    g_variant_unref(streams);

    g_print("PipeWire node id = %u  fd = %d\n", node_id, g_pw_fd);

    gchar *launch = g_strdup_printf("pipewiresrc fd=%d path=%u ! "
                                    "videoconvert ! xvimagesink", g_pw_fd, node_id);
    gst_init(nullptr, nullptr);
    GstElement *pipe = gst_parse_launch(launch, nullptr);
    gst_element_set_state(pipe, GST_STATE_PLAYING);
    g_print("正在播放,关闭窗口即退出…\n");
    g_free(launch);
}

int main(int argc, char *argv[])
{
    gtk_init(&argc, &argv);
    XdpPortal *portal = xdp_portal_new();
    g_loop = g_main_loop_new(nullptr, FALSE);

    create_session(portal);
    g_main_loop_run(g_loop);

    if (g_pw_fd >= 0) close(g_pw_fd);
    g_object_unref(portal);
    g_main_loop_unref(g_loop);
    return 0;
}

使用流的方式抓取一个图片(screencast_portal_oneframe.cpp)

/*
 * 基于 libportal 0.5 的「单帧截图」版本
 * g++ screencast_portal_oneframe.cpp \
    -I/home/uos/xxx/code/libportal/libportal-0.5/install/include \
    `pkg-config --cflags --libs gtk+-3.0 gstreamer-1.0` -lportal -o portal_png \
    -L/home/uos/xxx/code/libportal/libportal-0.5/install/lib/x86_64-linux-gnu
 */
#include <glib.h>
#include <libportal/portal.h>
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <unistd.h>
#include <ctime>


static GMainLoop *g_loop = nullptr;
static int g_pw_fd = -1;
static gchar *g_out_path = nullptr;

static void on_start_done (GObject *, GAsyncResult *, gpointer);
static void on_create_done(GObject *, GAsyncResult *, gpointer);

/* 1. 创建会话(老版 0.4 接口) */
static void create_session(XdpPortal *p)
{
    xdp_portal_create_screencast_session(
        p, XDP_OUTPUT_MONITOR,
        XDP_SCREENCAST_FLAG_NONE,
        XDP_CURSOR_MODE_EMBEDDED,
        XDP_PERSIST_MODE_NONE,
        nullptr, nullptr,
        on_create_done, p);
}

/* 2. 会话创建成功 → 直接 Start(老版无选设备) */
static void on_create_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpSession *session =
        xdp_portal_create_screencast_session_finish(XDP_PORTAL(src), res, &err);
    if (!session) { g_warning("create failed: %s", err->message); g_main_loop_quit(g_loop); return; }
    xdp_session_start(session, nullptr, nullptr, on_start_done, session);
}

/* 3. Start 完成 → 拿 PipeWire fd 并搭建单帧 pipeline */
static void on_msg_bus(GstBus *bus, GstMessage *msg, gpointer user_data)
{
    GstElement *pipe = GST_ELEMENT(user_data);
    switch (GST_MESSAGE_TYPE(msg)) {
    case GST_MESSAGE_ERROR:
        g_error("Pipeline error");
        g_main_loop_quit(g_loop);
        break;
    case GST_MESSAGE_EOS:               /* 单帧写完 */
        gst_element_set_state(pipe, GST_STATE_NULL);
        g_main_loop_quit(g_loop);
        break;
    default: break;
    }
}

static void on_start_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpSession *session = XDP_SESSION(src);
    if (!xdp_session_start_finish(session, res, &err))
    { g_warning("Start 失败: %s", err->message); g_main_loop_quit(g_loop); return; }

    int fd = xdp_session_open_pipewire_remote(session);
    if (fd < 0) { g_warning("no pw fd"); g_main_loop_quit(g_loop); return; }
    g_pw_fd = fd;

    /* 取第一个流节点 id */
    GVariant *streams = xdp_session_get_streams(session);
    guint node_id = 0;
    if (streams && g_variant_n_children(streams) > 0) {
        guint32 id; GVariant *props;
        g_variant_get_child(streams, 0, "(u@a{sv})", &id, &props);
        node_id = id; g_variant_unref(props);
    }
    g_variant_unref(streams);

    g_print("节点=%u  fd=%d  保存为:%s\n", node_id, fd, g_out_path);

    /* 4. 单帧 pipeline:pngenc + filesink */
    gchar *launch = g_strdup_printf(
        "pipewiresrc fd=%d path=%u num-buffers=1 ! "
        "videoconvert ! pngenc ! filesink location=%s",
        fd, node_id, g_out_path);
    gst_init(nullptr, nullptr);
    GstElement *pipe = gst_parse_launch(launch, nullptr);
    GstBus *bus = gst_element_get_bus(pipe);
    gst_bus_add_signal_watch(bus);
    g_signal_connect(bus, "message", G_CALLBACK(on_msg_bus), pipe);
    gst_element_set_state(pipe, GST_STATE_PLAYING);
    g_free(launch);
    gst_object_unref(bus);
}

int main(int argc, char *argv[])
{
    gtk_init(&argc, &argv);
    /* 默认保存路径 */
    // g_out_path = g_build_filename(g_get_user_home_dir(), "screenshot_portal.png", nullptr);
    g_out_path = g_strdup_printf("%s/screenshot_portal.png", g_getenv("HOME"));
    if (argc > 1) g_out_path = argv[1];

    XdpPortal *portal = xdp_portal_new();
    g_loop = g_main_loop_new(nullptr, FALSE);

    create_session(portal);
    g_main_loop_run(g_loop);

    if (g_pw_fd >= 0) close(g_pw_fd);
    g_object_unref(portal);
    g_free(g_out_path);
    return 0;
}

使用流的方式抓取(screencast_pull_stream.cpp)

/*
 * 基于 libportal-0.5 的「持续拉流」示例
 * g++ screencast_pull_stream.cpp \
    -I/home/uos/xxx/code/libportal/libportal-0.5/install/include \
    -L/home/uos/xxx/code/libportal/libportal-0.5/install/lib/x86_64-linux-gnu  \
    `pkg-config --cflags --libs gtk+-3.0 gstreamer-1.0 gstreamer-app-1.0` \
    -lportal -o portal_pull
 * ./portal_pull
 */
#include <glib.h>
#include <libportal/portal.h>
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
#include <unistd.h>

static GMainLoop *g_loop = nullptr;
static int        g_pw_fd = -1;
static GstElement *g_pipeline = nullptr;

/* 每帧回调:sample 里是 RGB 数据,可自由处理 */
static GstFlowReturn on_new_sample(GstAppSink *sink, gpointer)
{
    GstSample *sample = gst_app_sink_pull_sample(sink);
    if (!sample) return GST_FLOW_OK;

    GstBuffer *buf  = gst_sample_get_buffer(sample);
    GstCaps   *caps = gst_sample_get_caps(sample);
    /* 打印一帧信息 */
    gint width = 0, height = 0;
    GstStructure *s = gst_caps_get_structure(caps, 0);
    gst_structure_get_int(s, "width", &width);
    gst_structure_get_int(s, "height", &height);
    g_print("frame %dx%d  pts=%" GST_TIME_FORMAT "\n",
            width, height, GST_TIME_ARGS(GST_BUFFER_PTS(buf)));

    /* 这里可以把 buf 映射出来自己编码、推网络、存文件…… */
    gst_sample_unref(sample);
    return GST_FLOW_OK;
}

static void on_start_done(GObject *, GAsyncResult *, gpointer);
static void on_create_done(GObject *, GAsyncResult *, gpointer);

/* 1. 创建 screencast 会话 */
static void create_session(XdpPortal *p)
{
    xdp_portal_create_screencast_session(
        p, XDP_OUTPUT_MONITOR,
        XDP_SCREENCAST_FLAG_NONE,
        XDP_CURSOR_MODE_EMBEDDED,
        XDP_PERSIST_MODE_NONE,
        nullptr, nullptr,
        on_create_done, p);
}

/* 2. 会话创建成功 → start */
static void on_create_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpSession *session =
        xdp_portal_create_screencast_session_finish(XDP_PORTAL(src), res, &err);
    if (!session) { g_warning("create failed: %s", err->message); g_main_loop_quit(g_loop); return; }
    xdp_session_start(session, nullptr, nullptr, on_start_done, session);
}

/* 3. start 完成 → 搭 pipeline 持续拉流 */
static void on_start_done(GObject *src, GAsyncResult *res, gpointer)
{
    g_autoptr(GError) err = nullptr;
    XdpSession *session = XDP_SESSION(src);
    if (!xdp_session_start_finish(session, res, &err))
    { g_warning("Start 失败: %s", err->message); g_main_loop_quit(g_loop); return; }

    g_pw_fd = xdp_session_open_pipewire_remote(session);
    if (g_pw_fd < 0) { g_warning("no pw fd"); g_main_loop_quit(g_loop); return; }

    /* 取第一个流 node_id */
    GVariant *streams = xdp_session_get_streams(session);
    guint node_id = 0;
    if (streams && g_variant_n_children(streams) > 0) {
        guint32 id; GVariant *props;
        g_variant_get_child(streams, 0, "(u@a{sv})", &id, &props);
        node_id = id; g_variant_unref(props);
    }
    g_variant_unref(streams);
    g_print("拉流开始  node=%u  fd=%d  按 Ctrl-C 停止\n", node_id, g_pw_fd);

    /* 4. 持续拉流 pipeline */
    gchar *desc = g_strdup_printf(
        "pipewiresrc fd=%d path=%u ! "
        "videoconvert ! "
        "videoscale ! video/x-raw,format=RGB,width=1920,height=1080 ! "
        "appsink name=sink emit-signals=true max-buffers=1 drop=true",
        g_pw_fd, node_id);
    gst_init(nullptr, nullptr);
    g_pipeline = gst_parse_launch(desc, nullptr);
    GstAppSink *appsink = GST_APP_SINK(gst_bin_get_by_name(GST_BIN(g_pipeline), "sink"));
    g_signal_connect(appsink, "new-sample", G_CALLBACK(on_new_sample), nullptr);
    gst_element_set_state(g_pipeline, GST_STATE_PLAYING);
    g_free(desc);
}

int main(int argc, char *argv[])
{
    gtk_init(&argc, &argv);
    g_loop = g_main_loop_new(nullptr, FALSE);

    XdpPortal *portal = xdp_portal_new();
    create_session(portal);

    /* 运行直到用户 Ctrl-C 或调用 g_main_loop_quit */
    g_main_loop_run(g_loop);

    /* 清理 */
    if (g_pipeline) {
        gst_element_set_state(g_pipeline, GST_STATE_NULL);
        gst_object_unref(g_pipeline);
    }
    if (g_pw_fd >= 0) close(g_pw_fd);
    g_object_unref(portal);
    g_main_loop_unref(g_loop);
    return 0;
}