macOS桌面动态壁纸

macOS桌面动态壁纸
陈鑫宇macOS系统的桌面壁纸是一些240帧的视频,但只能在屏幕保护界面播放。
那么高清好看的壁纸居然不可以作为动态壁纸播放!太糟糕了!
市面上有很多动态壁纸的软件,但是他们大多软件功能复杂,操作繁琐,价格高昂,语言不便,并且自带下载视频,而不是播放系统壁纸视频。
项目已在GitHub开源,地址:Lumina
9个Swift文件,2995行,Xcode Command Line Tools编译,无需引第三方库。
同步发布软件包,可以直接下载使用。
第一次运行会自动寻找系统自带的Aerial素材,在~/Library/Application Support/com.apple.wallpaper/aerials/videos/下面,缓存的壁纸文件mov路径,也可以自备视频。
软件制作困难
遮挡
视频壁纸并未直接替代原壁纸图片,而是选择新建图层覆盖。
macOS的桌面有多层窗口。用CGWindowListCopyWindowInfodump出来如下:
| 层级 | owner |
|---|---|
| -2147483624 | 程序坞,系统自己的壁纸 |
| -2147483603 | 访达,桌面图标,铺满整块屏 |
| -2147483601 | 通知中心小组件 |
| 0 | 普通应用窗口 |
动态壁纸层需要位于系统壁纸层与桌面图标层之间。最终取值为-2147483610,比图标层低7层。
设置中存在名为“壁纸盖住桌面图标”的开关。
4K视频持续解码,需要在电池供电时暂停。电池状态通过IOKit获取:调用IOPSCopyPowerSourcesInfo获取电源信息列表,读取kIOPSPowerSourceStateKey;若其值等于kIOPSBatteryPowerValue,则判定为电池供电。
全屏模式检测不使用辅助功能。改为遍历窗口列表中layer == 0的窗口,取叠放顺序最靠前的窗口,计算其与屏幕的交集面积;若覆盖面积超过98.5%,则判定为全屏。
遮挡检测同样遍历窗口列表,但选项不同。窗口列表按叠放顺序提供,索引越靠前表示层级越高。因此需要查找位于壁纸窗口之前、layer >= 0且覆盖面积超过98%的窗口。此处不能添加.excludeDesktopElements;添加后会将壁纸窗口本身过滤掉,导致遮挡检测始终返回未被遮挡。全屏检测可以添加该选项,因为该场景本应忽略桌面层。
锁屏和屏幕休眠通过通知处理。在DistributedNotificationCenter上监听com.apple.screenIsLocked和com.apple.screenIsUnlocked,并在NSWorkspace上监听screensDidSleep和screensDidWake;事件到达时立即置位。电池和全屏状态每2秒轮询一次,仅在状态实际翻转时回调。
满足暂停条件时,日志记录一行“暂停播放(全屏应用 / 电池供电)”,并并列列出原因。
每块屏幕对应一个独立的NSWindow,通过遍历NSScreen.screens完成创建。插拔显示器或更改分辨率会发送didChangeScreenParametersNotification,且一次操作会连续发送多次;因此增加0.6秒防抖后再重建。
切换Space或激活其他应用后,偶尔会出现窗口不在当前Space的情况。在didActivateApplicationNotification和activeSpaceDidChangeNotification之后延迟0.35秒再次调用orderFrontRegardless。该数值经实验确定;若延迟更短,系统尚未完成窗口位置调整。
倍速
AVPlayer在2×以上存在硬性上限。使用macOS自带Aerial航拍素材测试(3840x2160、HEVC Main 10、239.76 fps),2.0×可稳定达到120帧/秒;调整至2.2×后降至每秒0至1帧,AVPlayerItem.canPlayFastForward也为false。仅相差0.2×即完全停止。
规避方法是将倍速预置到时间轴。使用AVMutableComposition将整段视频scaleTimeRange压缩至1/倍速 的时长,播放器仅以1×播放。
1 | let compressed = CMTimeMultiplyByFloat64(asset.duration, multiplier: 1.0 / speed) |
时间轴压缩后,修改倍速必须重建内容,而重建会导致播放进度归零。修正方法是在重建前记录源视频进度(currentTime × bakedSpeed),重建后seek到position / factor。
另有一个较不显著的问题:将“是否需要预置倍速”表示为布尔值并纳入配置指纹。3×、4×、5×均对应true,配置签名不变,因此不触发重建,始终按首次的3×播放。改为比较实际倍速值后行为正确。
重建还会导致短暂黑屏。replaceCurrentItem会清空图层缓冲。先移除旧层再创建新层,期间必然出现黑屏。使用旧层快照进行遮挡也不可行,因为AVPlayerLayer.contents恒为空,播放器不通过该通道输出。
最终采用双缓冲。保持两个(AVPlayer, AVPlayerLayer)实例,在不可见实例上更换item、执行seek并开始播放,轮询直到isReadyForDisplay为真且时间轴推进(最多等待1秒),然后交换可见性;此时才暂停旧实例并调用replaceCurrentItem(with: nil)释放解码器。轮播切换复用同一路径,手动“下一幅”同样不会出现黑屏。
另有一处与倍速相关的问题:AVPlayer.play()等价于rate = 1.0,因此先设置rate再调用play()会覆盖速度。表现为初始速度正确,但暂停后恢复时速度变为1×。所有起播路径统一改为playImmediately(atRate:)。
高倍速下卡顿是另一类问题。4×下出现周期性卡顿。最初认为由解码能力不足导致,但实测时间推进准确,CPU占用仅2.3%。实际原因是帧生成速率与上屏速率不匹配:30 fps素材在4×下需要120帧/秒,而AppKit窗口默认不以ProMotion的120 Hz满速运行,需要显式申请。macOS不提供CALayer.preferredFrameRateRange,该API属于iOS;只能使用NSView.displayLink。声明后可通过回调频率验证,日志可输出display link实测120.5 Hz。
5×属于物理限制。30 fps素材在5×下需要150帧/秒,120 Hz屏幕最多显示120帧/秒,必然丢帧。设置界面按“屏幕刷新率 ÷ 源帧率”计算该素材的最高无损倍速,超过该值时以橙色标记,无需逐档测试。
开机自启
开机自启配置写入~/Library/LaunchAgents/com.chenxinyu.lumina.plist。其中ProgramArguments包含--autostart,设置RunAtLoad,并将LimitLoadToSessionType设为Aqua。执行bootstrap前先执行一次bootout;否则重复切换自启开关时会返回service already loaded。
应用采用ad-hoc签名(codesign --sign -),不包含开发者证书。用户下载并解压后,Gatekeeper会拦截。处理方式有两种:在“系统设置 → 隐私与安全性”中选择“仍要打开”;或执行xattr -dr com.apple.quarantine /Applications/Lumina.app。该操作应在构建脚本中一并执行。
此外,将.app组装在mktemp创建的临时目录中,项目内仅保留zip文件。macOS 26的App库依据LaunchServices注册表收录bundle;只要项目目录中曾出现.app,就会与/Applications中的副本共同注册,导致显示两个图标。





