Moonlight 串流 + 电源 / 网络唤醒 / 双系统自动化实战
本文记录把一台闲置笔记本(下称 LAPTOP)改造成主力台式机(下称 HOST)扩展副屏的完整过程。
目标不是”远程桌面”,而是物理意义上多一块显示器:鼠标从主屏滑过去就进入 LAPTOP,窗口可以拖过去,行为等同接了一根 HDMI。
并在此之上实现:HOST 开机即亮、关机即省电、切换系统自动跟随,日常零操作。
目录
- 0. 摘要
- 1. 背景与动机
- 1.1 为什么不用现成方案
- 1.2 名词说明
- 2. 硬件与拓扑
- 3. HOST 侧:构造一路虚拟输出并单独采集
- 4. LAPTOP 侧:混合显卡环境下的解码问题
- 5. 配对与证书
- 6. 电源与网络唤醒自动化
- 7. 双系统无缝切换
- 8. 模式切换:副屏模式 / 独立模式
- 9. 远程运维
- 10. 性能与调优
- 11. 安全与取舍
- 12. 问题汇总(现象 → 原因 → 解法)
- 13. 经验与结论
- 14. 附:组件与可复用要点
- 15. 生成声明
0. 摘要
- 协议:HOST 运行 Sunshine(主机端采集 + 编码),LAPTOP 运行 Moonlight(客户端解码 + 显示)。
- 第二路输出的来源:HOST 上接入一个 HDMI 假负载(dummy plug,即俗称的”显卡欺骗”),使显卡多出一路输出;Sunshine 只采集这一路,不影响主屏。
- 跨系统:HOST 为 Windows + Linux 双系统,两套系统各运行一份 Sunshine,客户端自动选择当前在线的那个。
- 自动化:LAPTOP 上运行一个守护进程——HOST 在线时自动全屏拉流,离线时熄屏或挂起;HOST 开机时通过 Wake-on-LAN 唤醒睡眠中的 LAPTOP。
- 四个关键难点:
- 混合显卡环境下的解码链路问题(
queue overflow死循环); - WoL 组播发错网卡(魔术包从有线口发出,无法到达 WiFi 所在二层);
- 触摸板造成的虚假唤醒(挂起后约 15 秒自行唤醒);
- 双系统切换后的”僵尸流”(同机同 IP,客户端无法区分当前系统)。
- 混合显卡环境下的解码链路问题(
1. 背景与动机
起因很朴素:HOST 平时只有一块 27 寸主显示器,而在某些工作流里(写代码放参考资料、看行情放副图、游戏放攻略等)确实需要第二块屏。但:
- 再买一块显示器:占空间、要额外走线、成本也不低;
- 手边正好有一台闲置的游戏本 LAPTOP,性能与屏幕都还能用,平时却在吃灰。
于是目标变成:让这台闲置笔记本的屏幕变成台式机的第二块显示器。
有几个具体的考量:
- 尺寸匹配。这台笔记本的内屏,竖过来之后,正好和 27 寸主屏在视觉上尺寸相称,并排放置不会显得突兀——这也是最终采用竖屏布局的直接原因。
- 它本质上是一块”自带操作系统的屏幕”。这一点很重要:它不像普通显示器那样只能被动显示,而是可以本地运行程序、可以切换模式、可以做更多事情。副屏只是它的第一层用途,后续还有不少扩展空间(后文第 8 节的”独立模式”就是其中一种)。
目标确定后,剩下的是把它做到”每天早上开机就在那儿、不用管”的程度。
1.1 为什么不用现成方案
“用一块屏当另一台机的副屏”其实有几条现成路径:Deskreen、spacedesk、Weylus、各种 VNC,甚至用采集卡。选择依据如下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Sunshine + Moonlight | 硬编硬解、低延迟、开源、可指定采集哪一路输出 | 配置相对繁琐 |
| 各类 VNC | 部署简单 | 延迟高、颜色/刷新差、不适合当副屏 |
| 浏览器方案(Deskreen 等) | 跨平台方便 | 延迟高、CPU 占用重 |
| 采集卡 | 延迟最低 | 需要额外硬件、线缆 |
结论:要”当副屏用而不别扭”,硬件编解码是刚需;在这个方向上,Sunshine/Moonlight 是相对成熟的组合。
1.2 名词说明
| 名词 | 说明 |
|---|---|
| Sunshine | 主机端软件,负责屏幕采集与编码。 |
| Moonlight | 客户端软件,负责接收、解码与显示。 |
| 假负载 / dummy plug | 插在 HDMI 口上的小装置,内含一个假 EDID,使显卡认为”此处接有显示器”。 |
| NVENC / NvFBC | NVIDIA 的硬件编码 / 快速采集 API。 |
| WoL(Wake-on-LAN) | 网络唤醒,向睡眠中的主机广播魔术包使其唤醒。 |
| Vulkan / VAAPI / NVDEC | 显卡解码视频的几种后端,选择不当会导致卡顿乃至不可用。 |
| Tailscale | 组建虚拟内网的工具,使两台机器像在同一局域网,并自动尝试直连。 |
| systemd | Linux 的服务与进程管理器,负责开机自启与守护。 |
2. 硬件与拓扑
| HOST(主力) | LAPTOP(副屏) | |
|---|---|---|
| 角色 | Sunshine 主机 / 采集编码 | Moonlight 客户端 / 解码显示 |
| 系统 | Windows + Linux 双系统 | Linux(KDE Wayland) |
| 显卡 | 单张 NVIDIA 独显 | Intel 核显 + NVIDIA 独显(混合) |
| 屏幕 | 27 寸主显示器(横屏) | 内屏(竖屏) |
| 网络 | 有线 + WiFi | WiFi(另备有线口) |
| 组网 | Tailscale | Tailscale |
前提是两台机器网络互通。这里用 Tailscale 组了虚拟内网,好处是跨网段、出门也能用;但同局域网内有时存在端口隔离,直连会失败(见第 6、7 节)。
1 | ┌──────────── Tailscale 虚拟内网 ────────────┐ |
需要注意:方向必须对齐。LAPTOP 内屏是竖屏,因此假负载也设为竖屏、位置也放在左侧。否则鼠标滑过去时方向与位置全部错位,手感会很别扭。
3. HOST 侧:构造一路虚拟输出并单独采集
3.1 接入假负载
显卡默认不会向”未接设备的接口”输出画面。接入一个 HDMI dummy plug(内含假 EDID)后,系统便认为多了一台显示器。此时 HOST 的显示布局为:
1 | [ 假负载 竖屏 1080x1920 ] [ 主屏 横屏 2560x1440 ] |
在 Linux(KDE/X11)下用 xrandr 摆好:
1 | # 将假负载(HDMI-0)转为竖屏,置于主屏(DP-4)左侧 |
KDE 会记住该布局,下次登录自动恢复。若希望开机即生效、且不依赖 KDE,也可将其写入 kscreen 配置或一个 autostart 脚本。
3.2 让 Sunshine 只采集副屏
配置 sunshine.conf:
1 | locale = zh |
逐项说明:
output_name:核心参数。只采集副屏,主屏刷新率等完全不受影响。capture = nvfbc:使用 NVIDIA 采集。encoder = nvenc:使用 NVIDIA 硬件编码。packetsize = 1200:见第 10 节。minimum_fps_target = 60:否则静态画面时可能降到 30fps,运动画面会显得不连贯。stream_audio = disabled:作为副屏通常无需传输声音。
3.3 问题一:GeForce 上 NVFBC 默认被禁用
在消费级 GeForce 显卡上,NvFBC(快速采集 API)默认是禁用的。Sunshine 会退化为较慢、CPU 占用更高的 x11 采集方式,表现为”一开串流整机变卡”,且不会报错。
解决方式是使用社区补丁 nvidia-patch:
1 | # 解锁 NVFBC / NVENC(附带 alpm hook,驱动更新时自动重打) |
判断是否生效:查看 Sunshine 日志中的 Screencasting with NvFBC(出现即为已启用;若为 with X11 则未生效)。
3.4 问题二(Windows 专属):output_name 需要 device_id(GUID)
在 Windows 上写成:
1 | output_name = \\.\DISPLAY2 # 不生效 |
Sunshine 不会报错,而是静默回退到采集主屏——表现为副屏上显示的是被缩放的主屏内容、上下有黑边,容易被误判为”分辨率不对”。
正确做法是取 Sunshine 启动日志中 Currently available display devices 里目标屏的 device_id(GUID):
1 | output_name = {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} |
需要注意:Windows 与 Linux 的 output_name 格式完全不同(前者用 GUID,后者用 HDMI-0 这类连接器名)。双系统下两边的 sunshine.conf 是各自独立的,不能照抄。
3.5 网络与其它
- 使用 Tailscale 名而非 IP 引用主机,避免 DHCP 变动。
- 放行防火墙端口:
47984/47989/48010及若干 UDP 端口。 - 双系统下 Windows 与 Linux 是两个不同的 Tailscale 节点(名称不同),客户端需能区分(见第 7 节)。
4. LAPTOP 侧:混合显卡环境下的解码问题
在混合显卡笔记本上,Moonlight 首次连接后往往会出现如下日志循环:
1 | [SDL] Video decode unit queue overflow |
表现为画面卡死、花屏或延迟异常高。
4.1 成因
Moonlight 默认选择了 NVIDIA 独显进行 Vulkan 视频解码与渲染,但笔记本内屏挂在 Intel 核显上,导致每一帧都需要跨 GPU 拷贝;同时移动级 NVIDIA 显卡的 Vulkan 视频解码稳定性较差。
简言之:解码在一块 GPU、显示在另一块,帧在两者之间反复搬运,最终不堪重负。
4.2 解决:让解码与渲染全程留在核显,并采用 CPU 软解
1 | # ① 只向 Vulkan 暴露 Intel 核显(关键一步) |
实测:修改后 queue overflow 归零,画面流畅;CPU 占用约 45%,对一台四核笔记本而言完全可承受。
4.3 细节
VK_DRIVER_FILES必须指向intel_icd.json,不要选到intel_hasvk_icd.json(后者为 Haswell 老核显专用,会在新核显上报No Vulkan devices found)。这一点曾以grep intel | head -1的方式选错,浪费了不少时间——因为它按字母序排在前面。--resolution需与副屏一致(此处为 1080x1920),否则会被缩放、影响清晰度。--packet-size 1200见第 10 节。--no-vsync --no-frame-pacing用于降低延迟;Wayland 下由合成器兜底,不会撕裂。- 软解占用 CPU:若 LAPTOP 同时还有别的负载,可安装
intel-media-driver后改用核显 VAAPI 硬解。
这一条具有普适性:“解码在独显、显示在核显”是混合显卡笔记本的高频问题,凡使用 GPU 解码的软件都可能遇到;通用解法是避免跨 GPU。
5. 配对与证书
Moonlight 首次连接 Sunshine 需要配对:客户端生成一个 4 位 PIN,在 Sunshine 的 Web 后台(https://<host>:47990)中输入。
- 配对是一次性的,之后会记住。
- 双系统场景下,每套系统各配对一次即可。
- 若忘记 Web 后台密码,可重置:
1
sunshine --creds <用户名> <新密码>
如需自动化,也可直接调用其 Web API:GET /api/pin 获取 pairing_id,再 POST /api/pin(带 pin / pairing_id / name)完成,无需手动点击。
6. 电源与网络唤醒自动化
目标:
- HOST 在线 → LAPTOP 自动全屏拉流;
- HOST 离线 → LAPTOP 熄屏或挂起以省电;
- HOST 开机 → 唤醒睡眠中的 LAPTOP。
6.1 LAPTOP 上的守护进程
实现为一个循环(做成 systemd user 服务,随登录自启):
1 | # 伪代码 |
是否在线通过 tailscale status --json 判断。
6.2 熄屏与挂起的取舍
| 状态 | 功耗 | HOST 开机能否自动连 |
|---|---|---|
| 熄屏(关显示,系统运行) | 约 10W | 可以:守护仍在运行,可立即连接 |
| 挂起(S3 睡眠) | 约 1W | 不能:需要先被唤醒(依赖 WoL) |
因此,”极致省电”(挂起)与”开机可自动连”存在冲突:选择挂起,就必须具备唤醒能力。两种方式在实现中均可切换。
6.3 Wake-on-LAN:魔术包为何无效
唤醒睡眠中的机器,需要发送方广播一个魔术包(6 × 0xFF + 16 × 目标网卡 MAC)。实践中常失败,原因主要有两类。
问题三:魔术包发错网卡 / 跨网段
如果 HOST 与 LAPTOP 位于不同网段(中间隔着路由器),二层广播无法通过——WoL 是二层行为,路由器不转发广播。
更隐蔽的情况是:HOST 同时接入有线和 WiFi,默认路由走有线,于是魔术包从有线口发出,无法到达 LAPTOP 所在的 WiFi 二层:
1 | HOST: [有线 192.168.B.x] [WiFi 192.168.A.x] ← 默认路由走有线 |
解决:将发送 socket 绑定到”与目标位于同一二层”的那个源地址:
1 | # 找到与目标同网段的本地地址,绑定后再广播 |
最终实际采用的做法是:用一根网线将 LAPTOP 接入与 HOST 同一台路由器的 LAN 口,使两者处于同一二层,WoL 才稳定生效。“同一二层”是 WoL 的前提。
发送端(HOST)开机自动发送
- Windows:计划任务,使用开机触发(BootTrigger)+ SYSTEM,不依赖登录;脚本先等待本机获得目标网段的地址,再循环发送数次(覆盖 LAPTOP 的睡眠窗口)。
- 曾用”登录触发(onlogon)”,结果经常不触发(未登录时尤甚),改为开机触发后稳定。
- Linux:systemd system 服务,开机执行;脚本同样先等接口就绪,再绑定源地址发送,避免开机时 WiFi 尚未连接而发出空包。
问题四:虚假唤醒
配置完成后发现:LAPTOP 挂起后约 15 秒便会自行唤醒,无法真正入睡,表面上像”熄屏”,实际在反复睡眠与唤醒。
排查唤醒源:
1 | cat /sys/power/pm_wakeup_irq # 每次挂起后唤醒,究竟是谁触发的 |
结果显示每次都是触摸板(一个 ACPI/I2C HID 设备,名称形如 MSFT0001:01),而非网卡。于是禁用其唤醒能力:
1 | echo disabled | sudo tee /sys/bus/i2c/devices/i2c-<TOUCHPAD>/power/wakeup |
并将其做成开机服务以持久化。禁用后机器才能真正入睡;更重要的是,这才是判定 WoL 是否成功的干净环境:
唤醒后 pm_wakeup_irq |
含义 |
|---|---|
| 网卡的 PCIe PME 中断 | 由 WoL 唤醒 |
| 触摸板中断 | 由手动触碰或虚假唤醒触发 |
排查 WoL 前,应先将”非网卡的唤醒源”全部关闭,否则极易被”其实是手碰醒的”误导——它自己醒得比闹钟还勤。
6.4 小结
- WoL 只能在同一二层内生效;跨网段不宜强求,可考虑路由器或静态 ARP 等手段。
- 发送方需绑定到正确的源接口。
- 接收方应先排除虚假唤醒源。
- 成功与否以
pm_wakeup_irq为准。
7. 双系统无缝切换
HOST 是双系统,因此对应两个 Tailscale 节点(例如 host-pc-win 与 host-pc)。客户端需要:
- 判断当前哪个节点在线;
- 连接它;
- 当”应连的节点”发生变化时,重连。
问题五:同机同 IP,客户端无法区分系统
双系统的两个操作系统在同一台机器、同一张网卡上,获取到的局域网 IP 是同一个。若用 LAN IP 连接,客户端无法判断当前连的是哪个系统;加之 Tailscale 的在线状态存在滞后,甚至观察不到清晰的”全部离线”窗口。结果是:
- 从 Windows 切到 Linux(快速重启)时,指向旧系统的 Moonlight 进程僵留在原处未退出;
- 守护进程发现”已有串流进程”,便不再重连——表现为切换后副屏一直黑屏或无法连接。
解决:
- 使用每个节点唯一的 Tailscale IP(而非共享的 LAN IP)来标识”当前连接的系统”;
- 守护进程每轮比较”Moonlight 实际连接的 IP”与”当前应连的 IP”,不一致即杀掉重连;
- HOST”全部离线”时,同时清理僵尸进程。
由于 Tailscale 走的是直连(不经中继),使用 Tailscale IP 与 LAN IP 的性能差异可以忽略;为获得”可区分系统”这一能力,这一取舍是值得的。
8. 模式切换:副屏模式 / 独立模式
LAPTOP 不可能永远作为副屏——有时它本身就是一台普通笔记本。于是设计两个模式:
- 副屏模式:守护进程生效(自动连接 / 省电);关闭锁屏(唤醒后免密码,避免打断自动连接)。
- 独立模式:守护进程完全不动作;恢复锁屏(即一台正常笔记本,休眠后需密码)。
实现方式:
- 一个状态文件 + 一个切换脚本(
副屏/独立/ 循环); - 一个 KDE 面板小部件(左键循环,右键直接选择具体模式);
- 切换时联动锁屏设置(修改
kscreenlockerrc的Autolock/LockOnResume); - 另提供一个 “立即重连” 的手动入口(面板右键 / 应用菜单 / 命令行),以备自动逻辑异常时使用。
1 | 副屏模式:HOST 在线 → 自动连;离线 → 熄屏;锁屏 = 关 |
这也呼应了第 1 节的判断:它是一块自带操作系统的屏,因此”当副屏”与”当普通电脑”可以在同一个设备上按需切换。后续还能在此基础上做更多扩展。
9. 远程运维
整个过程基本是远程完成两套系统的配置的,依赖 Tailscale SSH:无需坐到 HOST 前,即可修改配置、查看日志、重启服务。
问题六:SSH 会话中无法使用图形认证代理
在 SSH 会话里执行 pkexec 做需要 root 的操作时,会报:
1 | Error executing command as another user: No authentication agent found. |
原因是 polkit 的图形认证代理运行在桌面会话中,而 SSH 会话不属于该 session。
解决:把需要 root 的操作打包成一个脚本,由用户在桌面上以 sudo bash xxx.sh 执行一次即可;或从桌面会话内部触发。
这也是自动化运维的常见边界:涉及交互式认证(sudo / polkit)时,最好将命令封装成脚本,交由人工确认执行。
10. 性能与调优
| 调优项 | 作用 |
|---|---|
--packet-size 1200 |
关键。Tailscale 的 MTU 为 1280,视频包超过会分片,进而卡顿;限制到 1200 后明显改善。 |
fec_percentage = 30 |
前向纠错,提升对 WiFi 丢包的容忍度(代价是略多带宽)。 |
minimum_fps_target = 60 |
防止静态画面降到 30fps。 |
nvenc_preset = 1 |
编码器最快档,降低编码延迟。 |
--no-vsync --no-frame-pacing |
客户端减少缓冲,降低延迟。 |
--bitrate |
局域网可提高码率换清晰度;WiFi 较弱则下调。 |
| 有线连接 | LAPTOP 能接网线就接,可显著降低抖动,对”顺滑”帮助最大。 |
延迟来源是”采集 → 编码 → 网络 → 解码 → 合成 → 显示”这一整条链路。逐段削减缓冲(去 vsync / 帧平滑、避免跨 GPU、限制包大小),才能从”流畅”提升到”顺滑”。
11. 安全与取舍
这套自动化会触及若干安全默认值,需要明确其影响:
- 关闭锁屏 / 免密码唤醒:副屏模式为便利而牺牲锁屏保护;独立模式会恢复。风险需自行评估。
- 关闭触摸板唤醒:为避免虚假唤醒,代价是挂起后无法用触摸板唤醒(可用键盘或电源键)。
- 开启 WoL:网卡在睡眠时保持可被魔术包唤醒。
- Tailscale SSH / 免密登录:便于远程运维,但持有私钥者即可登入。
- Web 后台密码:请设置足够强度。
总体原则:便利与安全存在权衡。将它们做成”按模式切换”——需要便利时享受便利,作为普通电脑使用时该锁就锁。
12. 问题汇总(现象 → 原因 → 解法)
| 现象 | 原因 | 解法 |
|---|---|---|
| 一开串流整机变卡 | GeForce 上 NVFBC 被禁用,退化为 X11 采集 | 使用 nvidia-patch |
| 副屏显示”缩放的主屏 + 黑边” | Windows 的 output_name 写法错误,回退到主屏 |
使用 device_id GUID |
客户端 queue overflow 死循环 |
混合显卡跨 GPU 解码 | 核显 VK_DRIVER_FILES + 软解 |
No Vulkan devices found |
选错 ICD(hasvk) | 选择 intel_icd.json |
| 卡顿、微顿 | Tailscale MTU 导致分片 | --packet-size 1200 |
| WoL 不生效 | 魔术包发错网卡 / 跨网段 | 绑定源地址;确保同二层 |
| 挂起后约 15 秒自醒 | 触摸板虚假唤醒 | 禁用其 power/wakeup |
| 开关机 / 切换系统后连不上 | 僵尸流 + LAN IP 无法区分系统 | 唯一 Tailscale IP + 目标变化重连 + 全离线清理 |
Sunshine 报 Address already in use 后不监听 |
旧实例占用端口 | 启动前清理残留(pkill + sleep) |
| 唤醒后没有网络 | WiFi 在恢复时掉链 | 走有线 / resume 钩子重启网络 |
SSH 中 pkexec 报无认证代理 |
polkit 代理在桌面会话 | 打包成脚本,桌面 sudo bash 执行 |
上文真正有技术含量的部分,多半集中在这些”看起来没问题、实际暗中失败“的问题上:配置本身查几行文档即可,难的是定位这些故障。
13. 经验与结论
- 先明确”各对象应处于什么状态”,再动手。 省电、自动连接、可唤醒三者之间的关系决定了整体架构。
- 把不可见的状态变为可观测。
pm_wakeup_irq、tailscale status、Sunshine 日志中的实际分辨率——应以观测为准,而非猜测。 - 环境耦合是最大的障碍。 硬编码的 IP、设备名、路径,换一台机器便失效;若要复用或开源,需先参数化。
- 使用二分法排除干扰。 排查 WoL 时先关闭触摸板唤醒,问题立即清晰。
- “看起来相同”的对象需要可区分。 双系统同机同 IP 是典型案例,解决思路是引入唯一标识。
- 每一步都留可回退路径。 配置文件保留
.bak,服务可disable,脚本可手动触发。
14. 附:组件与可复用要点
组件
- 主机:Sunshine(LizardByte)、
nvidia-patch - 客户端:Moonlight
- 组网:Tailscale(直连 + SSH)
- 显示:
xrandr(X11)/kscreen-doctor(KDE) - 自动化:systemd user/system 服务、Windows 计划任务、PowerShell
- 唤醒:WoL(魔术包)+
pm_wakeup_irq判据 - 模式:KDE 面板小部件 +
kscreenlockerrc
最值得单独复用的两点:
- 混合显卡笔记本上「核显渲染 + CPU 软解」以绕开解码问题;
- 「同一二层绑定源地址」的 WoL 正确发法,以及「关闭虚假唤醒源」的干净排查方法。
脱敏后的脚本骨架(示意)
1 | # client-connect.sh —— 客户端一键全屏连接(自动选择在线节点) |
1 | # host-watch.sh —— 守护:在线自动连 / 离线省电 / 目标变化重连(伪代码见 6.1) |
1 | # wol.py —— HOST 开机向 LAPTOP 发送魔术包(关键:绑定到同一二层的源地址) |
15. 生成声明
本文由 AI「大肥鱼」(DeepSeek)根据一次真实的折腾过程整理并撰写生成,并非作者本人所写。
文中所有主机名、IP、MAC、SSID、证书、路径等均已脱敏为占位符(如
HOST/LAPTOP/192.168.A.x)。
内容按便于阅读的方式做了组织,技术结论基于实际测试;但因硬件、系统与网络环境而异,照搬未必生效,请以自身环境为准。
涉及第三方工具的用法,请以官方文档为准。
(全文完)