English | 简体中文
把 MP4/MOV 视频变成一块会呼吸的桌面 —— 原生窗口沉到桌面层级,WKWebView + Metal 硬解,功耗低到几乎听不见。
基于上游 fork 深度改造:补齐图形界面、配置持久化、五种淡入效果与双引擎首帧提取。
macOS 原生不提供「用视频当壁纸」的官方入口,第三方方案要么常驻高功耗、要么无法在锁屏/睡眠时优雅让位,还常常在切换桌面空间(Mission Control)时窗口错位。
OnlyWallpaper 换了一条路:用一个无边框、忽略鼠标事件、沉到桌面图标层之下的原生 NSWindow 承载一块 WKWebView,由 WebKit 用 Metal 直接解码播放视频。窗口被加入「所有空间 + 固定 + 忽略循环切换」,因此切换桌面、全屏应用、Mission Control 时都不会被顶掉或抢焦点;系统睡眠、屏幕锁定时通过通知中心自动暂停,5 分钟无人操作也会自动暂停(可关)。
它不联网、不上传任何数据,只读取你选择的本地
.mp4/.mov文件;配置全部落在本机~/Library/Application Support/OnlyWallpaper/。
- 🎬 视频壁纸 —— 将
.mp4/.mov设为桌面背景,等比例object-fit: cover铺满并循环播放。 - 🚀 GPU 硬件加速 —— 由
WKWebView调用 Metal 解码,静止窗口 + 静音播放,实测功耗约 10–50mW(上游 README 口径)。 - 🖥️ 图形界面(新增) —— Fyne 深色主题双列布局,
osascript调起 macOS 原生文件选择器,2 秒轮询刷新运行状态。 - 💾 配置记忆与历史(新增) —— 自动保存上次视频与设置,保留最近 10 条视频历史,一键下拉复用。
- 🎨 五种淡入效果(新增) —— 黑色 / 模糊渐变 / 图片 / 视频第一帧 / 视频第一帧模糊渐变,切换到该空间时丝滑淡入。
- 🎞️ 双引擎首帧提取(新增) ——
ffmpeg与AVFoundation双通道,可指定或自动回退。 - 🔧 开机自启(增强) —— 生成
LaunchAgentplist,且自动补全当前全部设置(淡入、省电、首帧提取方法)。 - 📦 App 打包 ——
build_app.sh一键产出OnlyWallpaper.app。 - 🔒 单实例与自愈 —— 文件锁保证唯一实例;检测到残留旧进程会
SIGTERM→ 必要时Kill接管。
把下面这段提示词直接发给你的本地 AI Agent(Claude Code / Codex / OpenCode …):
请帮我安装并运行 OnlyWallpaper(macOS 视频动态壁纸工具,Go + Fyne + WKWebView)。
仓库:https://github.com/RayMorTwinkle/OnlyWallpaperv2
环境要求:macOS 10.14+、Go 1.25+、Xcode Command Line Tools(CGO 需要)。
可选:ffmpeg(用于首帧提取,未安装会自动回退到 AVFoundation)。
步骤:
1. 克隆:git clone https://github.com/RayMorTwinkle/OnlyWallpaperv2.git && cd OnlyWallpaperv2
2. 拉依赖:go mod tidy
3. 编译:CGO_ENABLED=1 go build -o wallpaper
4. 启动图形界面:./wallpaper --gui
(无参数直接运行 ./wallpaper 也会进入 GUI)
5. 在 GUI 里「浏览...」选择一段 .mp4/.mov,点击「启动壁纸」。
6. 打包成应用(可选):./build_app.sh → build/OnlyWallpaper.app
7. 向我确认壁纸已生效,并简述开销(Metal 解码、锁屏/空闲自动暂停)。# 1. 安装依赖(CGO 编译需要)
xcode-select --install
brew install ffmpeg # 可选,用于提取视频第一帧
# 2. 克隆与编译
git clone https://github.com/RayMorTwinkle/OnlyWallpaperv2.git
cd OnlyWallpaperv2
go mod tidy
CGO_ENABLED=1 go build -o wallpaper
# 3. 图形界面
./wallpaper --gui
# 或直接命令行启动
./wallpaper --video ~/Movies/loop.mp4环境要求:macOS 10.14+、Go 1.25+、Xcode Command Line Tools。
ffmpeg可选。 本项目使用 CGO 桥接 Objective-C,必须CGO_ENABLED=1。
| 命令 | 说明 |
|---|---|
./wallpaper --video <path> |
后台启动壁纸(自动 --daemon 转后台) |
./wallpaper --cmd stop |
停止当前壁纸 |
./wallpaper --cmd status |
查询是否正在运行 |
./wallpaper --cmd enable-autostart --video <path> |
写入 LaunchAgent 并登录自启 |
./wallpaper --cmd disable-autostart |
卸载自启 |
./wallpaper --gui |
打开图形界面 |
便捷写法:
./wallpaper stop/status/start会被normalizeArgs()自动补成--cmd形式。
| 参数 | 取值 | 默认 | 说明 |
|---|---|---|---|
--video |
路径 | — | .mp4 / .mov 文件路径,支持 ~/ 展开 |
--cmd |
start stop status enable-autostart disable-autostart |
start |
子命令 |
--daemon |
— | false |
后台运行(内部使用) |
--gui |
— | false |
启动 Fyne 图形界面 |
--debug |
— | false |
前台运行并打印详细日志 |
--no-idle-pause |
— | false |
禁用「5 分钟空闲自动暂停」 |
--opaque-window |
true / false |
true |
不透明窗口更省电;false 支持透明/模糊 |
--fade-in |
black transparent image firstframe firstframe-blur |
black |
淡入模式 |
--fade-in-image |
路径 | — | --fade-in image 时使用的图片 |
--extract-method |
auto ffmpeg avfoundation |
auto |
首帧提取引擎 |
# 用视频第一帧做淡入,自动挑选提取引擎(推荐)
./wallpaper --video ~/Movies/loop.mp4 --fade-in firstframe-blur
# 强制用 AVFoundation 提取首帧(无需安装 ffmpeg)
./wallpaper --video ~/Movies/loop.mp4 --fade-in firstframe --extract-method avfoundation
# 锁屏/空闲时不暂停(演示场景)
./wallpaper --video ~/Movies/loop.mp4 --no-idle-pause
# 开机自启并继承当前全部设置
./wallpaper --cmd enable-autostart --video ~/Movies/loop.mp4GUI 与 CLI 两条入口最终都汇聚到 main.go,再由 CGO 桥接到 Objective-C 渲染层;配置与视频历史由 config.go 独立持久化。
flowchart TB
subgraph ENTRY["入口层"]
GUI["GUI 模式<br/>gui.go · NewWallpaperGUI()"]
CLI["CLI 模式<br/>main.go · flag.Parse()"]
APP["OnlyWallpaper.app<br/>build_app.sh"]
end
subgraph CORE["主程序 main.go"]
NORM["normalizeArgs()"]
LOCK["单实例锁 flock<br/>/tmp/desktop-wallpaper.lock"]
START["startWallpaper()"]
BG["startBackground()"]
CMDS["stop / status / enable-autostart"]
end
subgraph CFG["配置 config.go"]
JSON["config.json<br/>+ firstframe.jpg"]
end
subgraph SYS["渲染层(CGO)bridge.m"]
BRIDGE["InitWallpaper()"]
WIN["NSWindow<br/>桌面层级"]
WV["WKWebView<br/>Metal 解码"]
HTML["assets/player.html"]
FF["ffmpeg"]
AV["AVFoundation<br/>ExtractVideoFirstFrame()"]
end
APP --> GUI
GUI --> START
CLI --> NORM --> LOCK --> START
CLI --> CMDS
START --> BG
START --> CFG
CFG --> JSON
START --> FF
START --> AV
START --> BRIDGE --> WIN --> WV --> HTML
GUI 与命令行共享同一份 Config;GUI 的改动既写回 JSON,也拼成 CLI 参数透传给后台进程,开机自启时再写进 plist。
flowchart LR
GUI["gui.go<br/>loadSettings() / Set*()"] --> CFG["Config 结构体"]
CFG --> P1["~/Library/Application Support/OnlyWallpaper/config.json"]
OLD["旧路径 ~/.onlywallpaper/config.json"] -. "LoadConfig() 迁移并删除" .-> P1
CFG --> ARGS["CLI 参数<br/>--fade-in / --extract-method ..."]
ARGS --> PLIST["~/Library/LaunchAgents/com.user.onlywallpaper.plist"]
GUI -->|onStart 拼参| ARGS
fade_in_mode 在 startWallpaper() 中被翻译成 fadeInFlag(0–4);需要首帧时先提取,失败则按模式回退。
flowchart TD
M["fade_in_mode"] --> Q{"取值?"}
Q -->|black| F0["fadeInFlag = 0<br/>黑色淡入"]
Q -->|transparent| F1["fadeInFlag = 1<br/>模糊渐变"]
Q -->|image| F2["fadeInFlag = 2<br/>图片淡入"]
Q -->|firstframe| F3["fadeInFlag = 3"]
Q -->|firstframe-blur| F4["fadeInFlag = 4"]
F3 --> EX["extractFirstFrame()"]
F4 --> EX
EX --> OK{"提取成功?"}
OK -->|是| IMG["fadeInImage = firstframe.jpg"]
OK -->|否| FB["回退:firstframe → black<br/>firstframe-blur → transparent"]
--extract-method auto 时优先 ffmpeg,命中失败自动回退 AVFoundation。
sequenceDiagram
autonumber
participant U as 用户
participant M as startWallpaper()
participant E as extractFirstFrame()
participant F as ffmpeg
participant A as AVFoundation
U->>M: --fade-in firstframe --extract-method auto
M->>E: extractFirstFrame(video)
E->>F: findFFmpeg() 命中 /opt/homebrew/bin
F-->>E: 写出 firstframe.jpg(-vframes 1 -q:v 2)
Note over E: 若 ffmpeg 不可用或失败
E->>A: ExtractVideoFirstFrame()<br/>AVAssetImageGenerator.copyCGImageAtTime
A-->>E: 写出 firstframe.jpg
E-->>M: 返回图片路径
M->>M: C.InitWallpaper(..., fadeInImage)
窗口生命周期由系统通知驱动:睡眠/唤醒、屏幕参数变化、应用退出,以及基于计时的空闲暂停。
sequenceDiagram
autonumber
participant OS as macOS
participant N as NSNotificationCenter
participant W as WKWebView
participant V as player.html
OS->>N: NSWorkspaceWillSleepNotification
N->>W: evaluateJavaScript("externalPause()")
W->>V: video.pause()
OS->>N: NSWorkspaceDidWakeNotification
N->>W: evaluateJavaScript("externalResume()")
W->>V: video.play()
OS->>N: NSApplicationDidChangeScreenParametersNotification
N->>W: setFrame(screenRect) 适配分辨率变化
Note over V: 空闲 300000ms 自动暂停,每 60000ms 检测一次<br/>受 {{DISABLE_IDLE_PAUSE}} 控制
config.json 的字段与 Go 结构体一一对应(JSON 键为 snake_case)。
erDiagram
CONFIG ||--o{ VIDEO_HISTORY : "最多 10 条"
CONFIG {
string last_video_path
bool autostart_enabled
bool no_idle_pause
bool opaque_window
string fade_in_mode
string fade_in_image
string frame_extract_method
}
OnlyWallpaperv2/
├── main.go # 入口:flag 解析、单实例锁、start/stop/status、首帧提取、自启
├── gui.go # Fyne 图形界面(深色主题、原生文件选择器、状态轮询)
├── config.go # 配置读写、旧路径迁移、视频历史(上限 10)
├── bridge.h # CGO 桥接头文件(InitWallpaper / ExtractVideoFirstFrame ...)
├── bridge.m # Objective-C:NSWindow + WKWebView + AVFoundation 首帧
├── assets/
│ └── player.html # 内嵌的视频播放模板(占位符 + 淡入 CSS/JS)
├── build_app.sh # 打包 OnlyWallpaper.app
└── go.mod # 模块依赖(fyne / flock)
编译产物
wallpaper与build/已在.gitignore/ 未纳入版本控制,此处不列出。
窗口层级与穿透。 configureWindow() 把窗口层级设为 CGWindowLevelForKey(kCGDesktopIconWindowLevelKey) - 1(沉到桌面图标层之下),并组合 CanJoinAllSpaces | Stationary | IgnoresCycle | FullScreenAuxiliary 的 collectionBehavior,同时 setIgnoresMouseEvents:YES 让点击穿透、setHasShadow:NO、setAnimationBehavior:NSWindowAnimationBehaviorNone。--opaque-window=true(默认)时窗口不透明、底色纯黑,最省电;false 时透明,配合模糊/透明淡入。
HTML 模板占位符。 renderHTMLToTempFile() 在 Objective-C 侧把模板里的占位符替换为真实值后写入临时文件:{{VIDEO_PATH}}、{{DISABLE_IDLE_PAUSE}}、{{FADE_IN_TRANSPARENT}}、{{FADE_IN_IMAGE}}、{{FADE_IN_IMAGE_BLUR}}。路径经 [NSURL fileURLWithPath:] 转成 file:// URL,规避中文/空格路径问题。
淡入 CSS 动画。 assets/player.html 定义四组 keyframes:fadeInBlack、fadeInTransparent(从 blur(8px) 到清晰,1.5s)、fadeInImage、fadeInImageBlur(从 blur(12px) 到清晰,2s)。切换到该桌面空间(visibilitychange)时重新施加动画。
双引擎首帧提取。 findFFmpeg() 依次探测 /opt/homebrew/bin、/usr/local/bin、/opt/local/bin、/usr/bin,最后回退 exec.LookPath。ffmpeg 命令为 -i <video> -vframes 1 -q:v 2 -update 1 -y <out>;AVFoundation 走 AVAssetImageGenerator(appliesPreferredTrackTransform=YES,容差 kCMTimeZero),JPEG 质量 0.85,输出到配置目录的 firstframe.jpg。
配置与兼容迁移。 主路径 ~/Library/Application Support/OnlyWallpaper/config.json;若读不到则尝试旧路径 ~/.onlywallpaper/config.json,读到后立即写回新路径并删除旧目录。LoadConfig() 对缺失的 fade_in_mode / frame_extract_method 填入默认值 black / auto。视频历史用「去重前置 + 截断到 10」维护。
单实例与自愈。 使用 github.com/gofrs/flock 对 /tmp/desktop-wallpaper.lock 加锁,并写入当前 PID。若加锁失败且能读到旧 PID,会 SIGTERM 后以 100ms 步进轮询最多 5 秒等待其退出,仍未退出则 Kill 强杀并重新加锁。
后台与日志。 startBackground() 会用 syscall.SysProcAttr{Setsid: true} 让子进程脱离会话,并把 stdout/stderr 重定向到 os.TempDir()/onlywallpaper.log。启动前先校验视频存在性、扩展名与内嵌模板完整性,确保错误能回显到前台。
开机自启。 写入 ~/Library/LaunchAgents/com.user.onlywallpaper.plist(Label: com.user.onlywallpaper,RunAtLoad=true),并把 --no-idle-pause、--opaque-window=false、--fade-in、--fade-in-image、--extract-method 按当前配置补齐后 launchctl load。
打包。 build_app.sh 以 CGO_ENABLED=1 go build -ldflags="-s -w" 产出可执行文件 Contents/MacOS/wallpaper,Info.plist 中 CFBundleIdentifier=com.onlywallpaper.app、LSMinimumSystemVersion=10.14,并生成启动脚本 exec wallpaper --gui。
Q:支持哪些视频格式?
A:仅 .mp4 与 .mov。两者都会写两份 <source>(video/mp4 与 video/quicktime)。
Q:为什么功耗这么低? A:窗口静止且不透明(默认纯黑),视频静音播放,解码交给 WebKit/Metal;锁屏与 5 分钟空闲自动暂停进一步降耗。
Q:没装 ffmpeg 能用「视频第一帧」淡入吗?
A:可以。--extract-method auto 会在 ffmpeg 不可用时自动回退到 AVFoundation(macOS 原生,无额外依赖)。
Q:配置文件在哪?卸载会残留吗?
A:在 ~/Library/Application Support/OnlyWallpaper/(config.json + firstframe.jpg),可被 AppCleaner 等卸载器识别清理。
Q:为什么必须 CGO_ENABLED=1?
A:项目用 CGO 桥接 Objective-C(bridge.m),链接 Cocoa / WebKit / CoreGraphics / AVFoundation / CoreMedia。
Q:提示「输入错误」或启动失败?
A:看 $(getconf DARWIN_USER_TEMP_DIR)(即 os.TempDir())下的 onlywallpaper.log,后台进程的日志都写在那里。
- 首次运行未签名版本可能触发 Gatekeeper,需在「系统设置 → 隐私与安全性」允许,或右键「打开」。
- 视频播放仍会占用一定 GPU,功耗随分辨率与帧率上升;上游口径约 10–50mW。
- 单实例运行:新实例会尝试接管/终止旧实例,请勿并发启动多份。
- 目前为单显示器适配(使用
[NSScreen mainScreen]);多显示器/每个屏幕独立壁纸尚未支持。 - 上游与本 fork 均无自动化测试;改动请以真机运行为准。
本 fork 沿用上游的 MIT License 声明。仓库当前未单独附带 LICENSE 文件(上游 README 标注 MIT,详见上游仓库)。(待确认:是否在本仓库补充 LICENSE 文件。)
- 上游:
Bronya0/OnlyWallpaper(@main),分叉基线在其8d0f581。 - 本 fork:
RayMorTwinkle/OnlyWallpaperv2,相对上游领先约 14 个提交(本地git rev-list计 15 个),仅 1 个文件属于琐碎改动。
上游原貌:纯 CLI(--video / --cmd / --daemon),单一 <video loop>,窗口恒为透明,空闲暂停硬编码 5 分钟且不可关,无配置文件、无 GUI、无淡入、无首帧提取;CGO 仅链接 Cocoa / WebKit / CoreGraphics,源码只有 8 个文件。
本 fork 的增量:
| 维度 | 上游 | 本 fork |
|---|---|---|
| 图形界面 | ❌ 无 | ✅ gui.go:Fyne 深色主题、双列布局、osascript 原生选文件、2s 状态轮询 |
| 配置持久化 | ❌ 无 | ✅ config.go:config.json + 旧路径迁移 + 视频历史(≤10) |
| 淡入效果 | ❌ 无 | ✅ 五种模式(black/transparent/image/firstframe/firstframe-blur) |
| 首帧提取 | ❌ 无 | ✅ 双引擎 ffmpeg + AVFoundation,--extract-method 可选/回退 |
| 省电开关 | 恒透明、空闲暂停不可关 | ✅ --opaque-window(默认不透明)与 --no-idle-pause |
| 开机自启 | 仅 --video --daemon |
✅ plist 自动补齐全部当前设置 |
| 窗口行为 | 基础 collectionBehavior | ✅ 追加 IgnoresCycle、屏幕参数变化响应、关闭动画 |
| 打包 | ❌ 无 | ✅ build_app.sh → OnlyWallpaper.app |
| CLI 友好度 | 基础 | ✅ expandPath() 支持 ~/、--debug 前台日志、stop/status 便捷子命令 |
| 依赖 | flock | + fyne.io/fyne/v2;LDFLAGS 增加 AVFoundation / CoreMedia |
历史说明:作者曾在一个提交中尝试「无缝循环(丝滑模式,双视频交替)」,随后在
0e15632(撤回)中回滚,当前代码不含该功能。故本文档不将其列为现有特性。
- 上游项目:Bronya0/OnlyWallpaper —— 本 fork 的全部基础(窗口层级方案、WKWebView 渲染、系统通知省电)均源自上游。
- Fyne —— 跨平台 Go GUI 框架。
- WKWebView —— macOS 视频渲染与 Metal 解码。
- AVFoundation —— 原生首帧提取。
- ffmpeg —— 可选的首帧提取工具。
- gofrs/flock —— 跨平台文件锁。
- 本仓库的中英双语 README 与架构图由本 fork 重制。