Sunshine 深度拆解:当 NVIDIA GameStream 闭源后,如何用 4 万颗 Star 重建自托管云游戏

关键词: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 编码器抽象出可插拔层,并对延迟进行极致优化。

本文按 介绍 → 原理 → 公式 → 图示 → 踩坑点 → 源码拆解 → 效果对比 的固定框架展开,重点回答:

  1. Sunshine 怎么用 RTSP + 自定义容器 把游戏画面 + 控制信号低延迟地端到端打通?
  2. NVENC / AMF / VAAPI / Vulkan Video / VideoToolbox 这五套 GPU 编码 API 的差异,Sunshine 怎么在 C++ 抽象层里把它们统一?
  3. 延迟 这个核心指标由哪几部分构成,硬件编码把哪一段从 30ms 砍到了 3ms?
  4. DXGI 桌面复制 / NvFBC / KMS / ScreenCaptureKit 这些捕获 API 的坑在哪里?
  5. Moonlight 的 NUT 容器 跟 MP4 / MKV 相比为什么更适合低延迟串流?
  6. 虚拟 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 APIIDXGIOutputDuplication 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.cppsrc/vaapi.cppsrc/amf.cppsrc/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 indexflags(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 与社区文档综合):

  1. 客户端发起 OPTIONS rtsp://<host>:47984 拿到服务器信息。
  2. 客户端发起 DESCRIBE 拿到 SDP 描述(包含 Sunshine 支持的编码器、分辨率、码率上限)。
  3. 客户端发起 SETUP 协商传输参数(RTP 端口、SSRC、payload type)。
  4. 服务端 PIN 配对:客户端首次连入产生一对临时 RSA 密钥,服务端用客户端公钥加密一个 4 位 PIN 给 Web UI 显示,用户在客户端输入 PIN 完成信任建立。之后客户端的 "unique ID"(其实是加密的 RSA 私钥)会持久化。
  5. 客户端 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 T_{capture} Tcapture 屏幕抓到 GPU texture 4 ~ 8ms 4 ~ 8ms(DXGI)
Tencode T_{encode} Tencode 编码器吐 IDR/P 帧 20 ~ 35ms 2 ~ 5ms
Tpack T_{pack} Tpack 切片到 NUT RTP 包 0.2 ~ 0.5ms 0.2 ~ 0.5ms
Tsend_buf T_{send\_buf} Tsend_buf 内核 sendto 排队 0.5ms 0.5ms
Tnetwork T_{network} Tnetwork LAN 内 RTT / 2 0.5 ~ 1ms 0.5 ~ 1ms
Trecv_buf T_{recv\_buf} Trecv_buf 客户端 buffer 1 ~ 2ms 1 ~ 2ms
Tdecode T_{decode} Tdecode 客户端硬解 2 ~ 4ms 2 ~ 4ms
Tdisplay T_{display} Tdisplay vsync 显示 8 ~ 16ms 8 ~ 16ms
总计 ~50ms ~22ms

注意 Tdisplay T_{display} Tdisplay 由显示器 vsync 决定,软件 / 硬件编码都逃不掉。一个 60Hz 显示器 vsync 周期是 16.67ms,如果你想要 < 16ms 端到端延迟,你必须配 120Hz / 144Hz / 240Hz 显示器 + 关闭 vsync 或用 G-Sync/FreeSync 的低延迟模式。这是 Sunshine 自己 README 的 Minimum Network 要求把 5GHz WiFi 列为 baseline 的根本原因:物理延迟的下限会被显示器拖死。

3.2 关键推论

从上面的拆解能直接得出几个工程结论:

  1. 硬件编码把 Tencode T_{encode} Tencode 从 30ms 砍到 3ms,是延迟优化的最大杠杆------这占端到端优化的 50% 以上收益。
  2. Tdisplay T_{display} Tdisplay 才是真正的天花板:显示器 vsync 一旦锁住,软件硬件编码的差距被显著压缩。
  3. 网络 Tnetwork T_{network} Tnetwork 在 LAN 下反而不是瓶颈------千兆网 RTT < 1ms,但如果你拿 WiFi 跑,5GHz AC 也能干到 3~5ms,WiFi 6E 能压到 2ms 以内。
  4. 客户端硬解 Tdecode T_{decode} Tdecode 在 AV1 上反而变成瓶颈 :iPhone / iPad 上的 AV1 硬解是从 A17 Pro / M2 起步才支持的,老设备只能软解,会让 Tdecode T_{decode} Tdecode 飙升到 15ms+。

3.3 码率(GOP / Slice / B 帧)对延迟的次要影响

视频编码的延迟除了来自"处理一帧的时间",还有"算法自身的参考依赖"。H.264 / HEVC / AV1 的 B 帧、双向预测、DCT 块大小都会影响:
Dalgorithm = DB−frame ⋅NB+ Dslice ⋅ Nslice + DGOP ⋅ NIDR D_{algorithm} = D_{B-frame} \cdot N_B + D_{slice} \cdot N_{slice} + D_{GOP} \cdot N_{IDR} Dalgorithm=DB−frame⋅NB+Dslice⋅Nslice+DGOP⋅NIDR

其中 DB−frame D_{B-frame} DB−frame 是 B 帧重排序延迟(一般 1 ~ 3 帧), Dslice D_{slice} Dslice 是 slice 切分重排序延迟(NVENC 上被压到 0,因为 NUT 一个 slice 一个包), DGOP D_{GOP} DGOP 是 GOP 长度对错误恢复的延迟(NVENC 默认 -zerolatency 模式下 NIDR =1 N_{IDR}=1 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 &params,
               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::optionalstd::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_tva_config_tva_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 协议的读者,我推荐的下一步:

  1. 读源码src/rtsp.cpp + src/nvhttp.cpp 是 Moonlight 协议最严肃的解释器,1500 行能看懂。
  2. 跑通一个完整链路:PC + Docker Sunshine + iPad Moonlight,跑通串流,再做"切到 Software 编码 → 切到 NVENC"的对比,会立刻理解本文的延迟分析。
  3. 挑一个 backend 慢慢看src/nvenc.cpp 是最成熟的,结构最清晰;src/vulkan.cpp 是 2026 年新的,值得追最新提交。
  4. 关注 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/vue v1.43.0
  • #5602 --- 配对流程重构 + 集成测试
  • #5368 --- libvirtualhid 接入,扩展游戏手柄
  • #5485 --- 日志文件轮转
  • #5675 --- Vulkan Video 队列族选择修复(2026-09-10)
  • #5554 --- AV1/HEVC probe 冲突修复(2026-09-10)
  • b55d74c --- Renovate 自动化依赖更新(2026-09-09)
相关推荐
Bmob后端云1 小时前
Bmob后端云实战|Python给备忘录接入AI摘要、文本润色功能
算法·github
RoboWizard1 小时前
游戏电脑不装固态硬盘会怎么样?
游戏·电脑
夜焱辰2 小时前
EO2Weave 浏览器扩展正式上架 Chrome 应用商店:你的 AI 助手,从此住进每一个标签页
github
驱动小百科3 小时前
英伟达GeForce Game Ready 616.92驱动发布:支持《007 First Light》等游戏
人工智能·游戏·英伟达616.92驱动·geforce game·nvidia显卡驱动
zzzzzz3104 小时前
anthropics/skills:高关注度“官方技能”项目,应该怎样读
人工智能·开源·github
职场的momo5 小时前
用户讨论:算法开发Offer决赛:大疆纯后端Agent与网易游戏测试,对内AI跳槽难?
人工智能·游戏·跳槽
阿里嘎多学长14 小时前
2026-09-09 GitHub 热点项目精选
开发语言·程序员·github·代码托管
面包狗AI4S14 小时前
GitHub AI4S 项目观察(2026-08-28—2026-09-03)
github
GoGeekBaird15 小时前
从「跑完即销毁」到「用完再销毁」:云沙箱的一次进化
后端·github·ai编程