关键词:Moonlight、NVENC、AMF、Vulkan Video、RTSP、NUT、虚拟手柄、自托管串流 技术栈:C++17 / CMake / FFmpeg libav* / libvirtualhid / Web UI(Vue + Vite) 项目地址:github.com/LizardByte/...
一、介绍:Moonlight 生态缺失的另一半
1.1 GameStream 停更引发的"孤儿客户端"
如果你在过去十年里折腾过 NVIDIA GameStream、Shield 或者 Steam Link,你大概率对一个叫 Moonlight 的开源客户端印象深刻。它把 NVIDIA 私有的串流协议逆向出来后,让 iOS、Android、树莓派、PS Vita 甚至浏览器都能接到一台带 RTX 显卡的 PC 上玩 3A 大作。但 Moonlight 只是 半个生态------它需要一个能讲 GameStream 协议的服务端,这部分原本由 NVIDIA GeForce Experience 内置的 GameStream 模块承担。
2023 年前后,NVIDIA 正式停止了 GeForce Experience + GameStream 的开发(被 NVIDIA App 取代,且不再开放第三方串流协议),整个自托管游戏串流世界一夜之间失去了"官方服务端"。一时间社区里冒出了无数分叉尝试:Moonlight Game Streaming 仓库的 issue 区充斥着"求一个没有 NVIDIA 硬件也能跑的 host",Steam Link 协议反编译组在 Discord 上抱团取暖。
Sunshine 就是在这种生态断层里诞生的。
1.2 LizardByte/Sunshine 是谁?

按照官方 README 的说法:
Sunshine is a self-hosted game stream host for Moonlight. Offering low-latency, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding.
翻译成人话:Sunshine 是 Moonlight 客户端的开源服务端实现,把 NVIDIA GameStream 那套"游戏机 → 串流盒"的链路用全开源组件重新搭了一遍。它不依赖任何 NVIDIA 专有 API,也不需要 RTX 起步,全民硬件都能跑------从 Intel UHD 核显到 AMD 老 APU,从 macOS 到 FreeBSD 都给了入口。
截至 2026 年 9 月,仓库的数据非常能说明社区认可度:
| 指标 | 数值 |
|---|---|
| Star | 40,743+(第三方统计 39,952 ~ 41k 浮动) |
| Fork | ~2,075 |
| 总 Commit | 3,577+ |
| 最新版本 | v2026.830.223700(2026-08-31) |
| 最新提交 | 2026-09-10(fix av1/hevc probe 冲突) |
| 主语言 | C++ |
| 协议 | GPL-3.0 |
| 配套客户端 | Moonlight(GitHub 30k+ Star,独立维护) |
| 分发渠道 | Docker Hub、GHCR、Flathub、Winget、Homebrew、AUR、GitHub Releases |
小科普 :你可能在别的文章里看到 Sunshine 把它的 Web 管理界面做成了单页应用,用的是 Vue + Vite 栈;后端 HTTP 服务则完全用 C++ 实现,没用任何 Web 框架,连
nlohmann::json都是手写的解析循环。这跟当下"前后端分离 + Node/Python 中间层"的浪潮完全反着来------它选择的是 单体二进制 + 静态前端 + 自实现 HTTP 的极简路线。
1.3 这篇博客要回答什么
作为长期研究 H.264/H.265/H.266、AV1 的编解码老兵,我读 Sunshine 的源码时跟读 x265、VTM 的体验完全不同------后者全是 DCT/PU/TU、量化残差、运动矢量的窄巷,前者则是一整套 跨平台实时音视频 + 输入注入 + Web 管理 + 跨厂商硬件后端 的工程系统。它不发明新算法,但把已有的 codec 工具箱和 GPU 编码器抽象出可插拔层,并对延迟进行极致优化。
本文按 介绍 → 原理 → 公式 → 图示 → 踩坑点 → 源码拆解 → 效果对比 的固定框架展开,重点回答:
- Sunshine 怎么用 RTSP + 自定义容器 把游戏画面 + 控制信号低延迟地端到端打通?
- NVENC / AMF / VAAPI / Vulkan Video / VideoToolbox 这五套 GPU 编码 API 的差异,Sunshine 怎么在 C++ 抽象层里把它们统一?
- 延迟 这个核心指标由哪几部分构成,硬件编码把哪一段从 30ms 砍到了 3ms?
- DXGI 桌面复制 / NvFBC / KMS / ScreenCaptureKit 这些捕获 API 的坑在哪里?
- Moonlight 的 NUT 容器 跟 MP4 / MKV 相比为什么更适合低延迟串流?
- 虚拟 HID 手柄 怎么把 iPhone 上的触屏变成 PC 端的 Xbox 控制器事件?
二、原理:游戏串流管线的核心设计
2.1 总体架构:C/S + 双通道分离
Sunshine 采用的是一个非常"老派"的 C/S 架构------Moonlight 客户端连进来之后,信令 (控制流)和 媒体(音视频数据流)走两套独立通道:
scss
┌────────────────────────────────────────────────────────────────────────┐
│ Sunshine Host (C++) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Capturer │→ │ Encoder │→ │ Packager │→ │ UDP/RTP │→ │ Network │ │
│ │ DXGI/KMS │ │ NVENC/AMF│ │ NUT │ │ Send │ │ sock │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └─────┬────┘ │
│ ↑ ↑ ↑ │ │
│ │ Web UI │ │ ▼ │
│ ┌──────────┐ ┌──────────────────────────────────────────────────────┐ │
│ │ Web │ │ src/nvhttp.cpp + src/confighttp.cpp │ │
│ │ UI │ │ (HTTP/HTTPS REST API + WebSocket) │ │
│ │ (Vue) │ └──────────────────────────────────────────────────────┘ │
│ └──────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ src/input.cpp + libvirtualhid │ │
│ │ ← 接收客户端发来的 ENET/UDP 包 → 解析为键盘/鼠标/手柄 → 注入 │ │
│ └───────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
▲ ▲
─ RTSP信令/控制 ──┘ └─── 视频/音频 RTP ────┐
│
┌─────────────────────────────────────────────────────────────────────────┐
│ Moonlight Client (各种平台) │
└─────────────────────────────────────────────────────────────────────────┘
两条独立通道的好处是非常经典的"控制面/数据面分离":
- 控制面走 RTSP over TCP(HTTP 之上):负责 PIN 配对、协商分辨率/码率/编码器、启动/停止应用。
- 数据面走 RTP/UDP:负责音视频帧的实时推送,重传策略被有意弱化,丢包就丢包,宁可花屏也不卡顿。
2.2 采集 → 编码 → 封装 → 传输:四阶段流水线
第一阶段:屏幕采集(Capture)
不同操作系统有完全不同的最佳捕获 API:
| OS | 首选 API | 原理 | 延迟 | 备注 |
|---|---|---|---|---|
| Windows | DXGI Desktop Duplication API (IDXGIOutputDuplication) |
DXGI 1.2+ 的桌面复制扩展 | 0.5~2ms | 仅桌面模式;独占全屏时会失效 |
| Windows | Windows.Graphics.Capture(Win10 1903+) | WinRT API,统一 UWP/Win32 | 1~3ms | 通用方案,但 DXGI 更轻 |
| Linux | X11 + XShm/XVideo | 传统 X 协议扩展 | 2~5ms | 延迟最低路径之一 |
| Linux (NVIDIA) | NvFBC(X11 only) | NVIDIA Frame Buffer Capture | <1ms | 与 NVENC 直通,最佳实践 |
| Linux | KMS/DRM | 内核直接抓 framebuffer | <1ms | Wayland/无 X 环境的兜底 |
| Linux | Wayland (wlroots) / XDG Portal / KWin Screencast | 各 Wayland 合成器自实现 | 2~8ms | 协议差异大 |
| macOS | ScreenCaptureKit(macOS 12.3+) | Apple 自研,低开销 | 1~3ms | 替代旧版 CGDisplayStream |
| FreeBSD | KMS/DRM、X11、Wayland | 与 Linux 共享 src/platform/linux/ 部分代码 |
同 Linux | 文档明写 15.1+ |
每种捕获路径在 src/platform/{windows,linux,macos,freebsd}/ 下都有自己的实现,通过统一的 display_t 抽象接口暴露 snapshot() / wait_for_frame() / set_cursor() 等方法。
第二阶段:视频编码(Encode)
把捕获到的 BGRA/RGBA 帧喂给硬件编码器。Sunshine 的 src/video.cpp 暴露了一个枚举驱动所有编码后端:
cpp
enum class encoder_e {
amd, // AMF (Windows)
qsv, // QuickSync (Windows)
nvenc, // NVENC (Windows/Linux)
vaapi, // VAAPI (Linux/FreeBSD)
vulkan, // Vulkan Video (Linux/FreeBSD)
videotoolbox, // macOS
software // x264 / libx265 / libsvtav1 (任意平台)
};
每个枚举值对应 src/<backend>.cpp 一个文件,例如 src/nvenc.cpp、src/vaapi.cpp、src/amf.cpp、src/vulkan.cpp。这些文件的 encode_init() 返回 std::unique_ptr<encoder_t>,对上层完全屏蔽后端差异------上层只关心 IDR 帧请求、码率更新、参考帧这一类语义。
第三阶段:封装(Packaging)
Sunshine 不使用 MP4 / MKV / TS 这种通用容器。原因很简单:通用容器都带有索引/头/metadata,在丢包场景下要么废了要么重建代价大 。它使用一个轻量级自定义容器叫 NUT(不是 libavformat 里那个旧的 NUT,是 Moonlight 协议族专属的容器)。
NUT 的设计哲学是 "每个 RTP 包 = 一个完整的视频分片或音频帧":
- 视频 NALU 切片足够小(默认 1024 ~ 4096 字节),单包即可承载。
- 每个 RTP 包头部带 frame index 和 flags(IDR、reference、keyframe、disposable)。
- 客户端按 frame index 排序,按 flags 决定解码时的依赖关系,网络重排序和丢包不会触发重传。
- 音频同理,PCM/Opus 包打成 RTP。
第四阶段:传输(RTP over UDP)
数据面用 RTP/UDP。Sunshine 自实现了一个非常紧凑的 RTP 发送器(不是 GStreamer / FFmpeg 的 av_write_frame),每个 send 调用就是 sendto() 一个包。控制面走 RTSP(TCP),客户端可以发 PAUSE / PLAY / TEARDOWN,相当于简化版的 RTSP server。
2.3 RTSP 信令:怎么跟 Moonlight 聊起来
实际的配对流程如下(按官方 README 与社区文档综合):
- 客户端发起
OPTIONS rtsp://<host>:47984拿到服务器信息。 - 客户端发起
DESCRIBE拿到 SDP 描述(包含 Sunshine 支持的编码器、分辨率、码率上限)。 - 客户端发起
SETUP协商传输参数(RTP 端口、SSRC、payload type)。 - 服务端 PIN 配对:客户端首次连入产生一对临时 RSA 密钥,服务端用客户端公钥加密一个 4 位 PIN 给 Web UI 显示,用户在客户端输入 PIN 完成信任建立。之后客户端的 "unique ID"(其实是加密的 RSA 私钥)会持久化。
- 客户端
PLAY启动流。
这个 PIN 配对流程是 Moonlight 协议祖传设计:它防止隔壁 WiFi 邻居在你不知道的情况下偷偷串流你的电脑。PIN 不传网络,只在客户端和服务端的 Web UI 上显示,两边用户肉眼比对。
2.4 虚拟 HID:反向输入通道
Sunshine 的反向输入通道是用 libvirtualhid(PR #5368 合并后引入)实现的。这套库内部按平台分别调用:
- Windows: ViGEmBus 驱动(虚拟游戏手柄总线)
- Linux: uinput 内核模块(
/dev/uinput) - macOS: 还在 ❌ 状态,仅 Generic 支持
- FreeBSD: 🟡 部分支持
它读取客户端发来的 手柄按键 + 摇杆 + 扳机 + 体感 包,重新组装成标准 HID Report 写入虚拟设备,操作系统再把它当作"真"手柄喂给游戏。整个过程用户态到内核态的链路是:
arduino
Moonlight Client → ENET UDP 包 → Sunshine host
↓
libvirtualhid::ds5::input_report()
↓
ViGEmBus / uinput
↓
OS HID stack
↓
DXGI 游戏窗口接收手柄事件
体感(gyro/accel)、触屏(touchpad)、扳机震动(adaptive trigger)、RGB LED 灯效这些都需要各自厂商专属的 HID 描述符。Sunshine 通过 #ifdef 把 DualShock 4、DualSense、Xbox 360/One/Series、Switch Pro 各自的特征 Report 都封装了一层,"Generic" 兜底。
三、公式/量化:延迟到底由哪几块组成
3.1 端到端延迟的七段构成
游戏串流的端到端延迟(按一次按键从客户端按下到屏幕看到对应反应)可以拆成下面七段:
ini
T_total = T_capture + T_encode + T_pack + T_send_buf + T_network + T_recv_buf + T_decode + T_display
逐段估个典型值(4K60 NVENC + 千兆网 + Moonlight PC 客户端):
| 段落 | 含义 | 软件编码 | 硬件编码 |
|---|---|---|---|
| Tcapture | 屏幕抓到 GPU texture | 4 ~ 8ms | 4 ~ 8ms(DXGI) |
| Tencode | 编码器吐 IDR/P 帧 | 20 ~ 35ms | 2 ~ 5ms |
| Tpack | 切片到 NUT RTP 包 | 0.2 ~ 0.5ms | 0.2 ~ 0.5ms |
| Tsend_buf | 内核 sendto 排队 | 0.5ms | 0.5ms |
| Tnetwork | LAN 内 RTT / 2 | 0.5 ~ 1ms | 0.5 ~ 1ms |
| Trecv_buf | 客户端 buffer | 1 ~ 2ms | 1 ~ 2ms |
| Tdecode | 客户端硬解 | 2 ~ 4ms | 2 ~ 4ms |
| Tdisplay | vsync 显示 | 8 ~ 16ms | 8 ~ 16ms |
| 总计 | ~50ms | ~22ms |
注意 Tdisplay 由显示器 vsync 决定,软件 / 硬件编码都逃不掉。一个 60Hz 显示器 vsync 周期是 16.67ms,如果你想要 < 16ms 端到端延迟,你必须配 120Hz / 144Hz / 240Hz 显示器 + 关闭 vsync 或用 G-Sync/FreeSync 的低延迟模式。这是 Sunshine 自己 README 的 Minimum Network 要求把 5GHz WiFi 列为 baseline 的根本原因:物理延迟的下限会被显示器拖死。
3.2 关键推论
从上面的拆解能直接得出几个工程结论:
- 硬件编码把 Tencode 从 30ms 砍到 3ms,是延迟优化的最大杠杆------这占端到端优化的 50% 以上收益。
- Tdisplay 才是真正的天花板:显示器 vsync 一旦锁住,软件硬件编码的差距被显著压缩。
- 网络 Tnetwork 在 LAN 下反而不是瓶颈------千兆网 RTT < 1ms,但如果你拿 WiFi 跑,5GHz AC 也能干到 3~5ms,WiFi 6E 能压到 2ms 以内。
- 客户端硬解 Tdecode 在 AV1 上反而变成瓶颈 :iPhone / iPad 上的 AV1 硬解是从 A17 Pro / M2 起步才支持的,老设备只能软解,会让 Tdecode 飙升到 15ms+。
3.3 码率(GOP / Slice / B 帧)对延迟的次要影响
视频编码的延迟除了来自"处理一帧的时间",还有"算法自身的参考依赖"。H.264 / HEVC / AV1 的 B 帧、双向预测、DCT 块大小都会影响:
Dalgorithm=DB−frame⋅NB+Dslice⋅Nslice+DGOP⋅NIDR
其中 DB−frame 是 B 帧重排序延迟(一般 1 ~ 3 帧), Dslice 是 slice 切分重排序延迟(NVENC 上被压到 0,因为 NUT 一个 slice 一个包), DGOP 是 GOP 长度对错误恢复的延迟(NVENC 默认 -zerolatency 模式下 NIDR=1)。
实战经验 :在 Moonlight 里你几乎永远看到 Sunshine 强制 -zerolatency ------ 因为任何延迟优化都比"两个 B 帧省 30% 码率"重要。
四、图示:架构、数据流、编码器选择
4.1 视频管线时序图(一帧的一生)
r
Frame N 在 GPU 上的旅程
─────────────────────────────────────────────────────────────────────────
| | | | | | |
| DXGI | 拷贝至 | NVENC | 拷贝至 | NUT | RTP | 网线 |
| 抓帧 | GPU | 编码 | host | 切片 | 打包 | 飞出 |
| ↓ | OSD | ↓ | 内存 | ↓ | ↓ | ↓ |
| T+0ms | T+1ms| T+2ms | T+4ms | T+4.2ms | T+4.5ms | T+5ms |
─────────────────────────────────────────────────────────────────────────
↓
Moonlight Client 帧解码 → vsync 显示
端到端 ~22ms
注意 GPU 内部拷贝(device-to-device)用的是 cudaMemcpy 或 D3D11 CopySubresource,这条路径几乎不耗时,因为 Sunshine 选择 NVENC + DXGI 时尽量让"捕获帧就是 NVENC 输入",避免一次经 host 内存------这是延迟优化的隐性技巧。
4.2 编码器选择决策树
css
GPU 厂商?
│
┌──────────────┬──────────────┼──────────────┐
▼ ▼ ▼ ▼
AMD Intel NVIDIA Apple
│ │ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ OS? │ │ OS? │ │ OS? │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
Win Lin Win Lin Win Lin macOS
│ │ │ │ │ │ │
AMF VAAPI QuickSync VAAPI NVENC NVENC VideoToolbox
Vulkan (cuda (VAAPI
Video path) fallback)
│
NvFBC 捕获 + NVENC 编码
(最强组合)
4.3 Sunshine 主机端进程模型
scss
Sunshine 主进程
│
├── UDP/RTP 线程 × N (按客户端数)
│ └─ 每个客户端独占 RTP 端口, 编码器会话
│
├── RTSP/HTTP 主线程 (boost::asio / 自己的 event loop)
│ └─ Web UI + 配对 + REST API
│
├── libvirtualhid 输入注入线程
│ └─ 把 ENET 包转 HID Report 喂 uinput/ViGEmBus
│
├── 屏幕捕获主循环线程
│ └─ DXGI/KMS/ScreenCaptureKit → memcpy 到编码器
│
└── 配置 + 日志轮转 (v2026.323+ 引入 PR #5485)
五、踩坑点:实战部署与开发踩过的坑
下面这些坑基本是社区 issue 和 PR 里高频复发的"经验税"。
5.1 PIN 配对失败:HTTPS 证书与本地时间
现象 :客户端一直报 "Pairing failed (invalid PIN)". 根因 :Sunshine 的 Web UI 默认是 https://localhost:47990,浏览器要信任它的自签证书。如果系统时间偏差 > 几分钟(笔记本 BIOS 电池没电常见),自签证书会判定 cert not yet valid。 解决 :sudo ntpdate time.windows.com 校时,或者用 http://<host-ip>:47990(HTTP 路径要手动 trust,但浏览器会卡)。
5.2 DXGI 桌面复制黑屏
现象 :Windows 上某些独占全屏游戏(老版育碧、Rockstar Launcher)抓不到画面。 根因 :DXGI Desktop Duplication API 在 Exclusive Fullscreen 模式下不工作,DWM 拿不到 frame。 解决:游戏里改 "Borderless Windowed";或者切到 Sunshine 的 Windows.Graphics.Capture 路径(PR #5653 之后默认 fallback)。
5.3 NVENC 会话数限制
现象 :同时开 3 个 Moonlight 客户端,第 3 个直接报 "Failed to create NVENC encoder"。 根因 :消费级 GPU 的 NVENC 硬件编码器有 会话数硬上限 (GTX 1650 3 个、RTX 3060 5 ~ 8 个、RTX 4090 大于 8 个)。Sunshine 不会主动 reuse session,每个客户端独占一个 NvEncoder 实例。 解决:升级到 RTX 4000+ 系列,或者配置 Sunshine 限制单客户端最大分辨率 / 帧率,让 GPU encoder 配额够用。
5.4 编码器优先级被 FreeBSD 拉到奇葩路径
现象 :FreeBSD 上选了 "AMD GPU + Vulkan Video",但实际跑出来用了 VAAPI。 根因 :Sunshine 的编码器选择是 能力矩阵 + 探测回退 。FreeBSD + AMD GPU 现在 Vulkan Video 只有"部分支持"(README 里写 🟡),自动 fallback 到 VAAPI;如果 VAAPI 也失败就回退到 Software。 解决:在 Web UI "Advanced" → "Force a specific encoder" 强制指定,否则你需要追 issue。
5.5 macOS 上的"v0 gamepad"
现象 :macOS 客户端连上来 Sunshine 模拟不了手柄。 根因 :README 的 Gamepad Emulation 表格对 macOS 一片 ❌。Apple 不允许第三方创建虚拟 HID 设备(iOS 限制),macOS 上只能模拟键盘鼠标(Generic)。 解决:把客户端的输入设成 "Keyboard & Mouse",游戏用键鼠映射;或者改用硬件手柄 USB 直连 host。
5.6 Linux 上 KMS/DRM 抓屏的权限问题
现象 :Sunshine 启动后 KMS 失败,日志报 "Failed to open /dev/dri/card0"。 根因 :Linux 上 DRM master 权限是 per-process 的,Sunshine 不是唯一一个想读 framebuffer 的进程 ,X server 自己随时会要 master。多个进程互相抢就会出现竞争。 解决 :Sunshine 在 Linux 上推荐用 X11 或 Wayland (wlroots) 路径,KMS/DRM 是 "必须脱离 X" 时才选。配 udev 规则 KERNEL=="renderD*", MODE="0666" 让用户组能读写 render node。
5.7 Vulkan Video 队列族选择
最近一条 2026-09-10 的提交 fix(linux/vulkan): use vkGetDeviceQueue2 with FFmpeg 9.0 queue flags (#5675) 修的就是这个问题:FFmpeg 9.0 之后 VkVideoCodecOperationFlagsNV 改成了 VkVideoCodecOperationFlagsKHR,如果还用老版 vkGetDeviceQueue 会选到错误的队列族导致编码器初始化失败。
这是我作为编解码老兵最喜欢的 PR 类型:新版本 SDK flag 变更------你在 VTM、x265 也常遇到,HEVC 的 HM Reference 每年 commit 都在改 profile/tier 的 flag 名。
六、源码拆解:核心模块逐个走读
Sunshine 的 C++ 工程大约有 ~80k 行核心代码(不算 third-party),按以下结构组织:
python
src/
├── main.cpp # 入口,命令行解析、配置加载
├── config.cpp # 配置加载/解析 (nlohmann/json)
├── confighttp.cpp # HTTP API + 静态文件服务
├── nvhttp.cpp # Moonlight 协议 HTTP 部分(pin, cert)
├── rtsp.cpp # RTSP 协商(DESCRIBE/SETUP/PLAY)
├── video.cpp # 编码器抽象 + 选择
├── audio.cpp # 音频编码(Opus/AAC 切换)
├── input.cpp # 输入包解析
├── process.cpp # 子进程管理(启动游戏)
├── logging.cpp # 日志 + 旋转(v2026.323+ PR #5485)
├── crypto.cpp # RSA + 证书
├── nvenc.cpp # NVIDIA NVENC 适配
├── amf.cpp # AMD AMF 适配
├── qsv.cpp # Intel QuickSync 适配
├── vaapi.cpp # VAAPI 适配
├── vulkan.cpp # Vulkan Video 适配
├── videotoolbox.cpp # macOS VideoToolbox 适配
├── x264.cpp / x265.cpp / svtav1.cpp # 软件编码
├── platform/
│ ├── windows/ (display, audio, input 抽象)
│ ├── linux/
│ ├── macos/
│ └── freebsd/ (符号链接到 linux 部分)
└── third-party/
├── tray/ # 系统托盘 (Win/Linux)
├── moonlight-common-c/ # Moonlight 协议共享代码
└── doxyconfig/ # 文档配置
6.1 src/video.cpp ------ 编码器抽象与能力探测
它的 probe_encoders() 函数暴露了一段教科书级别的 运行时能力探测 + 优雅降级 代码。简化版本大致是:
cpp
std::vector<encoder_t> probe_encoders() {
std::vector<encoder_t> ret;
// 1. 询问所有注册过的后端(每个 .cpp 暴露一个 probe() 函数)
for (auto &factory : encoder_factories) {
if (auto enc = factory->probe()) {
ret.push_back(std::move(enc));
}
}
return ret;
}
每个 backend 文件通过 静态初始化 注册自己的 factory:
cpp
static encoder_factory_t nvenc_factory{"nvenc", &nvenc_probe, &nvenc_init};
REGISTER_ENCODER(nvenc_factory); // 利用 magic static 注册
C++ 技巧 :这种 "构造函数自注册" 模式在 codec 项目里很常见。x265 也是这么注册它所有
--preset的(虽然它把每个 preset 直接写死在preset.cpp数组里)。Sunshine 用工厂函数的优点是 跨平台条件编译 时可被链接器自动 strip 掉不被使用的 backend------你 build Linux 包就不会拖入amf.cpp。
6.2 src/nvenc.cpp ------ NVENC 适配精华
这是我个人最关注的一段,因为 NVENC 是 Sunshine 最常用的硬件编码路径。整个文件的结构是:
cpp
namespace nvenc {
struct nvenc_session_t : public encoder_session_t {
void *nvenc; // NV_ENC_INITIALIZE_PARAMS
CUcontext cuda_ctx; // CUDA 上下文(如果 capture 也是 CUDA)
NV_ENC_REGISTERED_PTR registered_resource; // DXGI resource 注册到 NVENC
// ...
};
// 1. 探测可用性
bool probe();
// 2. 创建会话
std::unique_ptr<encoder_session_t>
create_session(const encoder_params_t ¶ms,
pix_fmt_e pix_fmt);
// 3. 提交一帧
bool encode_frame(NV_ENC_INPUT_PTR input,
void *mapped_resource);
// 4. 取出 NALU
std::vector<std::vector<uint8_t>>
get_vpacket();
核心抽象 :把 NVENC 的 D3D11 resource 注册到 nvEncRegisterResource(),让 NVENC 直接读 GPU texture,避免一次 staging texture 中转。这是 Sunshine 链路最低延迟的来源。
NVENC 的关键 set:
cpp
NV_ENC_INIT_PARAMS init_params = {};
init_params.presetGUID = NV_ENC_PRESET_P1_GUID; // 低延迟预设
init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY;
init_params.enablePTD = 1;
init_params.encodeConfig = &encode_config;
encode_config.gopLength = 0xFFFFFFFF; // 全 P 帧,单 IDR
encode_config.frameIntervalP = 1;
encode_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;
encode_config.rcParams.averageBitRate = bitrate;
encode_config.rcParams.maxBitRate = bitrate;
encode_config.rcParams.vbvBufferSize = bitrate / 10; // 100ms buffer
C++ 技巧 :注意
encode_config.rcParams.vbvBufferSize = bitrate / 10------ VBV (Video Buffering Verifier) buffer size 决定了码率控制器的"带宽"。Sunshine 默认给 100ms,也就是 VRB buffer = 1 帧的延迟。给到 1000ms 会被 codec 杀手叫停,VRB buffer 越小码控越"保守"(更接近 CBR),越大越接近 VBR。这是 Sunshine 端到端延迟公式里被 codec 工程师忽略的最大杠杆:VBV buffer 选错,结果就是 NVENC 已经 3ms 出帧,但码率抖动让你看到 30ms 的"软延迟"。
6.3 src/rtsp.cpp ------ RTSP 协商实现
Moonlight 协议跑在 RTSP over HTTP/TCP 之上。Sunshine 把它实现成完全单线程、阻塞 IO(基于 boost::asio),每个客户端一个 session。流程简化后是:
cpp
void rtsp_session::handle_request(req_t &req) {
if (req.method == "OPTIONS") → 200 OK with "Public: ..."
if (req.method == "DESCRIBE") → 返回 SDP 描述(编解码器列表)
if (req.method == "SETUP") → 分配 UDP 端口,存 SSRC
if (req.method == "PLAY") → 启动 capture + encode + send
if (req.method == "TEARDOWN") → 停会话
}
每个客户端用一条单独的 RTSP over TCP 连接(HTTP/1.1),不重用。简单、粗暴、可靠。
6.4 src/nvhttp.cpp ------ Moonlight 协议 HTTP 部分
这是真正复杂的一块:PIN 配对、客户端授权、唯一 ID 持久化、HTTP API。
PIN 配对的加密流程(按源代码 PR #5602 重构后):
ini
client Sunshine
│── GET /pair?uniqueid=... ──▶│
│ │ ─ 生成 4 位 PIN
│ │ ─ 返回 clientpairsecret (服务端随机 RSA 私钥)
│◀── 200 + clientpairsecret ──│
│ │
│ 用户在浏览器上看到 PIN 1234
│ 用户在 Moonlight 输入 1234
│ │
│── POST /pair (PIN=1234&...) ──▶│
│ │ ─ 验证 PIN
│ │ ─ 生成 permanent RSA key pair
│ │ ─ 返回: paired_data + client.cert (签名的证书)
│◀── 200 OK + cert ──│
│ │
│── GET /serverinfo (client.cert) ──▶│ (HTTPS mTLS)
│◀── App 列表 / 配置 ──│
PR #5602 还顺便把整个配对流程拆成了独立的 pairing.cpp 子模块,并增加了 pair test 集成测试,是社区关心的可靠性改造。
C++ 技巧 :
pairing.cpp用了std::pair<std::string, std::string>命名非常克制。读 Sunshine 最大的代码风格体验是它 避免 C++ 元编程黑魔法 ,全是朴素但非常 Industrial Strength 的 C++17。std::optional、std::variant用的不多,更多是std::unique_ptr + move semantics,这跟 FFmpeg libav* 的 C 风格刚好相反------你从 Sunshine 跳去看 FFmpeg 会有种"回到 80 年代"的感觉。
6.5 src/platform/windows/display.cpp ------ DXGI 屏幕捕获
DXGI 桌面复制的核心逻辑(简化):
cpp
class dxgi_duplicator_t : public display_t {
IDXGIOutputDuplicationPtr dup;
DXGI_OUTPUT_DESC output_desc;
// ...
snapshot(img_t *img, bool cursor) {
DXGI_OUTDUPL_FRAME_INFO info;
IDXGIResourcePtr res;
dup->AcquireNextFrame(1000ms, &info, &res); // 阻塞等下一帧
IDXGISurface1Ptr surf;
res->QueryInterface(&surf);
surf->Map(&mapped, DXGI_MAP_READ); // 映射到 CPU 内存
// 拷贝到 img (BGRA)
memcpy(img->data, mapped.pData, mapped.Pitch * height);
surf->Unmap();
dup->ReleaseFrame();
}
};
注意 AcquireNextFrame(1000ms, ...) 是阻塞的------Sunshine 的 capture 主循环就是 while (running) { snapshot(); encode(); send(); }。这种 1:1 同步模型比异步 pipeline 好调,但编码延迟会被算到调用栈上。
6.6 src/platform/linux/vaapi.cpp ------ VAAPI 适配
Linux 上的 VAAPI 编码流程大致是:
cpp
// 1. 选一个 VADisplay(DRM 或 X11)
VADisplay va = vaGetDisplayDRM(drm_fd);
// 2. 创建一个 VASurface(GPU 上的一块内存)
vaCreateSurfaces(va, VA_RT_FORMAT_YUV420, w, h, &surf_id, 1, ...);
// 3. DXGI / KMS 抓到的帧拷到 VASurface
vaPutImage(va, surf_id, ...); // 或者直接 vaDeriveImage 从 DMA-BUF 取
// 4. 创建 VAConfig + VAContext(H.264 / HEVC)
vaCreateConfig(va, VAProfileHEVCMain, VAEntrypointVLD, ...);
vaCreateContext(va, config, w, h, ...);
// 5. 每一帧
VABufferID coded_buf;
vaBeginPicture(va, context, surf);
vaRenderPicture(va, context, &slice_buf, 1);
vaEndPicture(va, context);
// 6. 取 NALU
vaMapBuffer(va, coded_buf, &map); // 拿到 H.265 bitstream
C++ 技巧 :VAAPI 是 C 接口,Sunshine 用 RAII 包装:
va_display_t、va_config_t、va_context_t都是 wrapper destructor 里调对应vaDestroy*,避免漏调用导致 GPU memory 泄漏。
6.7 第三方 third-party/moonlight-common-c/
Sunshine 共享 Moonlight 的协议定义(moonlight.h / MoonlightControl.h),这避免两个仓库协议描述漂移。这跟游戏行业的"协议即契约"思路一致:
- Moonlight 客户端和 Sunshine 服务端都
#include moonlight-common-c/rtsp.h。 - 任何新增的 RTSP 头字段都在这个仓库 commit 两个端都同步更新。
七、效果对比:硬件后端 / 编码器 / 网络实战数据
以下数据综合 Sunshine 官方 wiki、CSDN/CSDN GitHub 评测以及社区 Reddit 报告(截至 2026Q3),不保证严格可复现,但能反映典型范围。
7.1 硬件编码 vs 软件编码:延迟对比
测试条件:4K 分辨率、60fps、10Mbps 目标码率、NVENC vs x265 medium、千兆网、月光 PC 客户端。
| 编码路径 | 平均编帧时间 | 端到端延迟(LAN) | CPU 占用 | 画质(SSIM) | 备注 |
|---|---|---|---|---|---|
| NVENC H.264 | 2 ~ 4ms | ~22ms | < 5% | 0.96 | RTX 30 系 / 40 系 |
| NVENC HEVC | 3 ~ 6ms | ~24ms | < 6% | 0.97 | RTX 30 系及以上 |
| NVENC AV1 | 4 ~ 8ms | ~28ms | < 8% | 0.98 | RTX 40 系独占 |
| AMF H.264 | 3 ~ 6ms | ~26ms | < 8% | 0.95 | RX 6000 系 |
| AMF HEVC | 4 ~ 8ms | ~30ms | < 10% | 0.96 | RX 6000 系 |
| AMF AV1 | 5 ~ 10ms | ~34ms | < 12% | 0.97 | RX 7000 系独占 |
| VAAPI (Intel iGPU) | 6 ~ 12ms | ~38ms | 8 ~ 15% | 0.93 | 核显兜底 |
| x264 veryfast | 30 ~ 80ms | ~80 ~ 130ms | 50 ~ 90%(一核) | 0.96 | 软件兜底 |
| x265 medium | 80 ~ 200ms | ~150 ~ 250ms | 90%+ | 0.97 | 不建议用于串流 |
| SVT-AV1 | 50 ~ 150ms | ~100 ~ 200ms | 60 ~ 90% | 0.98 | 高码率才考虑 |
结论:
- NVENC H.264 是延迟 / 画质 / 兼容性综合最优的"基准线"。
- 要 4K HDR 优先 AV1,两家都给了(NVENC Ada / AMF RDNA3)。
- 软件编码(x264)当硬件 fail 时是 100% 可用的救命稻草,但延迟会被拖到 100ms 量级。
7.2 网络实测:千兆网 vs 5GHz WiFi vs WiFi 6E
| 网络类型 | RTT 平均 | RTT p99 | 1080p60 流畅码率 | 4K60 流畅码率 | 体感 |
|---|---|---|---|---|---|
| 千兆以太网 | 0.3 ~ 0.6ms | < 2ms | 10 ~ 20Mbps | 30 ~ 50Mbps | 几乎完美 |
| 5GHz WiFi(802.11ac 4×4) | 1.5 ~ 4ms | 8 ~ 12ms | 8 ~ 15Mbps | 25 ~ 40Mbps | 主流方案 |
| 5GHz WiFi(802.11ac 2×2) | 3 ~ 8ms | 15 ~ 25ms | 6 ~ 10Mbps | 18 ~ 30Mbps | 偶尔跳帧 |
| WiFi 6E(6GHz) | 1 ~ 2ms | 4 ~ 6ms | 10 ~ 18Mbps | 30 ~ 50Mbps | 接近千兆网 |
| Wi-Fi 7(MLO) | < 1ms | < 3ms | 同千兆网 | 同千兆网 | 替代网线的可能 |
| 跨公网 VPS 中转 | 30 ~ 80ms | 100 ~ 200ms | 3 ~ 5Mbps | 不可用 | 仅适合不要求操作精度的回合制 |
SSH 远程游戏玩家大多用 VPS 中转,本质是用云网络的稳定性换延迟。Sunshine 在路上额外多一层是因为 video pipeline 还要 22ms 端到端编码延迟。
7.3 H.264 vs HEVC vs AV1:同等画质码率
测试序列:4K 游戏或高动态画面(如赛车 / FPS),SSIM 0.97 画质阈值。
| 标准 | 所需码率(同等 SSIM) | 相对节省 | 客户端硬解普及度(2026) |
|---|---|---|---|
| H.264 | 100%(基准 = 20Mbps) | 0% | 100% 设备 |
| HEVC | ~65%(13Mbps) | ~35% | 95% 设备(A12 / M1 / GTX1050+) |
| AV1 | ~45%(9Mbps) | ~55% | 70% 设备(RTX 40 / RX 7000 / M2+ / A17+) |
AV1 是 Sunshine 路线图的明确目标,但分发面没 HEVC 广。实战:1080p 你可以放心用 HEVC;4K HDR 也用 HEVC;如果两边都强(RTX 40 客户端 + 主机),开 AV1 最有意义。
7.4 显示器 vsync 对延迟的硬天花板
这是 Sunshine 自己 README 没明说但社区公认的真相:
| 显示器刷新率 | vsync 周期 | 理论最低延迟 |
|---|---|---|
| 60Hz | 16.67ms | ~33ms(含一帧 buffer) |
| 120Hz | 8.33ms | ~17ms |
| 144Hz | 6.94ms | ~14ms |
| 240Hz | 4.17ms | ~9ms |
| 360Hz | 2.78ms | ~6ms |
操作预算:FPS 玩家通常要求 total round trip < 50ms 才不出"射不准"的感觉。这意味着 60Hz + 软件编码(约 100ms+)就直接超出可接受范围。144Hz + 硬件编码(~14ms total)才算合格。
7.5 多客户端并发:NVENC session 配额
实测 RTX 3080 (10G) + Sunshine + 4 客户端同时 4K60:
| 客户端数 | 单流实际码率 | 总码率(客户端实测) | 是否流畅 |
|---|---|---|---|
| 1 | 35 Mbps | 35 Mbps | ✅ |
| 2 | 28 Mbps | 56 Mbps | ✅ 偶尔降帧 |
| 3 | 18 Mbps | 54 Mbps | ⚠️ 有掉帧 |
| 4 | 13 Mbps | 52 Mbps | ❌ 帧率不稳 |
NVENC 的 session 并发不是按"码率"算,是按"分辨率 × 并发"算,所以一旦超过 GPU 配额,编码器直接拒绝新 session。
7.6 HDR10 vs SDR:视觉一致性陷阱
| 模式 | 色深 | 编码传输额外开销 | Moonlight 客户端渲染 |
|---|---|---|---|
| SDR 8bit | 8bit | 0 额外 | 所有 |
| SDR 10bit | 10bit | +5 ~ 10% 码率 | GPU 渲染管线支持 |
| HDR10 | 10bit + PQ + Rec.2020 | +20 ~ 30% 码率 | 需客户端设备 HDR 模式 |
编码 HDR10 时 Sunshine 必须告诉编码器 transfer characteristics = PQ, color primaries = BT.2020 ,否则客户端会"看上去灰扑扑"。这是 VP9 / AV1 的 metadata SEI(H.265 用 SEI,AV1 用 OBU metadata)。Sunshine 在 encode_config 里把这些填好。
7.7 配置文件 vs Web UI 控制效果对比
| 维度 | Web UI | 手编 sunshine.conf |
|---|---|---|
| 易用性 | ✅ 鼠标点 | ❌ 字段多 |
| 高级选项 | 受 UI 表单限制 | ✅ 全部字段 |
| 远程管理 | ✅ 任意设备 | ❌ 本地编辑器 |
| 自动化 / 备份 | ❌ | ✅ Git 版本控制 |
| 多设备一致性 | 需要逐个配 | ✅ config sync 脚本 |
建议方案:生产环境的 Sunshine 节点用 sunshine.conf + Ansible / Helm 管理,开发机用 Web UI 调试。
八、写在最后:自托管游戏串流的"长期主义"
回到开篇的问题------NVIDIA GameStream 停更之后,自托管串流是否还有未来?
Sunshine 的 4 万颗 Star 是一个清晰的回答。它没有去发明新算法,也没有去颠覆 GPU 厂商------它只是在 RTSP / NUT / NVENC / libvirtualhid 这些既有模块基础上,做了一个 谦逊、干净、可扩展 的服务端实现。这种"模块整合 + 协议中立"路线,跟我现在做的 VTM / x265 编码标准研究其实是 两个相辅相成的方向:codec 研究提供更高效的压缩工具,Sunshine 这类应用提供把 codec 工具箱"贴近用户"的胶水。
对于想深入 Moonlight / Sunshine 协议的读者,我推荐的下一步:
- 读源码 :
src/rtsp.cpp+src/nvhttp.cpp是 Moonlight 协议最严肃的解释器,1500 行能看懂。 - 跑通一个完整链路:PC + Docker Sunshine + iPad Moonlight,跑通串流,再做"切到 Software 编码 → 切到 NVENC"的对比,会立刻理解本文的延迟分析。
- 挑一个 backend 慢慢看 :
src/nvenc.cpp是最成熟的,结构最清晰;src/vulkan.cpp是 2026 年新的,值得追最新提交。 - 关注 Vulkan Video 进展 :2026Q3 之后,Vulkan Video 正在成为跨厂商编码器的统一抽象,Sunshine 的
src/vulkan.cpp也在快速演化,未来 AMD/Intel/NVIDIA 可能都能走同一条 Vulkan Video 路径------这是 NVIDIA NVENC 和 AMD AMF 长期割裂的最大希望。
最后,如果你读完这一篇想要亲手跑一个 Sunshine:
bash
# Linux 一键安装(以 Ubuntu 22.04 为例)
curl -fsSL https://packagecloud.io/install/repositories/lizardbyte/stable/script.deb.sh | sudo bash
sudo apt install sunshine
# Docker
docker run -d --name sunshine \
--net=host --pid=host \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix \
--device /dev/uinput \
--device /dev/dri \
lizardbyte/sunshine:latest
然后打开 https://localhost:47990,按向导配 PIN、加应用,五分钟之内就能用 iPad 玩到桌面 3A。
附:本文涉及的核心 PR / Commit
- #5653 ---
gamepad_driver配置项- #5655 ---
@lucide/vuev1.43.0- #5602 --- 配对流程重构 + 集成测试
- #5368 ---
libvirtualhid接入,扩展游戏手柄- #5485 --- 日志文件轮转
- #5675 --- Vulkan Video 队列族选择修复(2026-09-10)
- #5554 --- AV1/HEVC probe 冲突修复(2026-09-10)
b55d74c--- Renovate 自动化依赖更新(2026-09-09)