概述
基于项目需要,原来的程序运行在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…
实现
- 通过libporta调用dbus接口,进而调用"org.freedesktop.portal.Desktop"服务,进行屏幕抓屏;
- 为了实现一个平台编译多平台运行,选择uos的x11编译,实现在x11、wayland平台的运行;
- uos上编译因为缺少libportal的预编译版本,所以自己编译。
- 当然,要实现一次编译多出运行,必须识别桌面协议类型,是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;
}