在 Gnome 中创建一个虚拟显示器,然后将其它电脑变成副屏

· · 科技·工程

事情是这样的.我希望在教师机上用机房管理软件播放自己 ubuntu 笔记本上的内容,最好是作为副屏,方便我放课件的时候看备注(pympress).然后就有了这么个邪修 workflow.

首先是创建一块虚拟显示器.这一步我折腾了好久.因为我用的是 wayland,查到的好多资料都只在 x11 上可用.ai 向我推荐了内核级的虚拟显示器,但是都不太好用,gnome 压根不识别.

查找了 100 年,我发现 一篇文章 介绍了 ubuntu 远程桌面可以创建一块虚拟屏幕,让其他电脑通过 rdp 连接.但是很遗憾,我这里不能显示鼠标光标.

但是这说明 gnome 肯定是支持在用户侧创建一个虚拟显示器的.我选择直接询问 ai 是怎么实现的,原来是有一个专门的 dbus 接口.由于我不熟悉操作 dbus,直接让 ai 给我写了个脚本.

#!/usr/bin/env /usr/bin/python3

import sys
import argparse
import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop

import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst

DBusGMainLoop(set_as_default=True)
Gst.init(None)

loop = GLib.MainLoop()
pipeline = None

def main():
    global pipeline

    parser = argparse.ArgumentParser(description="Create a virtual monitor in GNOME Wayland")
    parser.add_argument("-W", "--width", type=int, default=1920, help="Width of the virtual monitor")
    parser.add_argument("-H", "--height", type=int, default=1080, help="Height of the virtual monitor")
    parser.add_argument("-f", "--fps", type=int, default=60, help="Framerate of the virtual monitor")
    parser.add_argument("-v", "--view", action="store_true", help="Show the virtual monitor in a local window (useful for testing)")
    args = parser.parse_args()

    bus = dbus.SessionBus()
    screen_cast_iface = 'org.gnome.Mutter.ScreenCast'
    screen_cast_session_iface = 'org.gnome.Mutter.ScreenCast.Session'

    # 1. 连接到 Mutter 的 ScreenCast API
    try:
        screen_cast = bus.get_object(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
    except dbus.exceptions.DBusException as e:
        print(f"Failed to connect to Mutter ScreenCast API: {e}")
        print("Are you running GNOME on Wayland?")
        sys.exit(1)

    print("1. Creating screen cast session...")
    session_path = screen_cast.CreateSession([], dbus_interface=screen_cast_iface)
    print(f"   Session path: {session_path}")
    session = bus.get_object(screen_cast_iface, session_path)

    print(f"2. Requesting virtual monitor creation ({args.width}x{args.height}@{args.fps})...")
    # 属性配置:is-platform 表示捕获整个 stage 渲染,cursor-mode 1 表示嵌入鼠标指针
    properties = dbus.Dictionary({
        'is-platform': dbus.Boolean(True, variant_level=1),
        'cursor-mode': dbus.UInt32(1, variant_level=1)
    }, signature='sv')

    # 核心 API:RecordVirtual
    stream_path = session.RecordVirtual(properties, dbus_interface=screen_cast_session_iface)
    print(f"   Stream path: {stream_path}")
    stream = bus.get_object(screen_cast_iface, stream_path)

    def terminate():
        global pipeline
        if pipeline is not None:
            print("\nStopping GStreamer pipeline...")
            pipeline.send_event(Gst.Event.new_eos())
            pipeline.set_state(Gst.State.NULL)
        print("Stopping Mutter session...")
        session.Stop(dbus_interface=screen_cast_session_iface)
        loop.quit()

    def on_message(bus, message):
        t = message.type
        if t == Gst.MessageType.EOS or t == Gst.MessageType.ERROR:
            terminate()

    def on_pipewire_stream_added(node_id):
        global pipeline
        print(f"3. PipeWire stream added! Node ID: {node_id}")

        # 如果指定了 --view,则在本地打开一个窗口显示虚拟屏幕内容;否则使用 fakesink 仅保持连接
        sink = "autovideosink" if args.view else "fakesink"

        # 关键点:通过 video/x-raw 的 caps 限制,告诉 PipeWire (及 Mutter) 我们需要的分辨率
        pipeline_str = (
            f'pipewiresrc path={node_id} ! '
            f'video/x-raw,max-framerate={args.fps}/1,width={args.width},height={args.height} ! '
            f'videoconvert ! {sink}'
        )
        print(f"   Starting pipeline: {pipeline_str}")
        pipeline = Gst.parse_launch(pipeline_str)
        pipeline.set_state(Gst.State.PLAYING)
        pipeline.get_bus().connect('message', on_message)
        print("✅ Virtual monitor is now active! Check your GNOME Display Settings.")

    # 监听 PipeWire 流就绪信号
    stream.connect_to_signal("PipeWireStreamAdded", on_pipewire_stream_added)

    print("4. Starting session...")
    session.Start(dbus_interface=screen_cast_session_iface)
    print("Press Ctrl+C to destroy the virtual monitor and exit.")

    try:
        loop.run()
    except KeyboardInterrupt:
        print("\nInterrupted by user.")
        terminate()

if __name__ == '__main__':
    main()

然后就是共享桌面了.我选择的是 deskreen,在浏览器里就能直接使用.

如果你愿意花十块钱买一个 HDMI 欺骗器就可以省去第一步.反正我不愿意.