Reference: the .livewallpaper v1 format
Format version: 1 (frozen). Verified against PrimoEngine 0.4.11, macOS 26.
File layout
A ZIP (store or deflate compression) containing:
manifest.json required, at the root (or inside ONE single top-level folder)
content/ required, the wallpaper payload
thumbnail.png optional, thumbnail shown in the app (not covered by the checksum)
Other root files are ignored. The extension may be .livewallpaper or .zip.
manifest.json
| Field | Type | Req. | Notes |
|---|---|---|---|
schemaVersion | number | yes | Always 1. |
id | string | yes | Non-empty. Stable identity; importing another package with the same id replaces the installed one. Use reverse-DNS (com.yourname.name) or a UUID. |
version | string | yes | e.g. "1.0.0". Must be a string (not a number). |
title | string | yes | Non-empty. Shown in the app. |
author | object | no | { "id": string?, "handle": string? }. |
type | string | yes | "metal", "web" or "video". |
entry | string | yes | Path of the main file from the package root, e.g. content/shader.metal. Must exist. |
minMacOS | string | no | e.g. "26.0". On an older Mac the import fails. |
checksum | string | recommended | sha256-… over content/ (algorithm below). If present it must match, on import and on every load. |
config | array | no | User settings (table below). |
capabilities | object | no | Table below. |
Unknown fields are ignored. A .metal needs type: "metal", an .html type: "web", a video
type: "video". A mismatched type doesn’t fail the import, but the wallpaper renders black.
config[]
| Field | Type | Notes |
|---|---|---|
key | string | Required. Name of the value delivered to the wallpaper. |
type | string | float, int, bool, color. Anything else (including enum) is ignored. |
label | string | Control label. Default: key. |
min, max | number | float/int. Defaults 0 and 1. |
default | number / bool / string | float/int: number (default = min); bool: true/false (default false); color: "#RRGGBB" (default "#FFFFFF"). |
int is shown as a continuous slider and delivered as a decimal number: round it in your code.
capabilities
| Field | Type | Notes |
|---|---|---|
network | array of strings | Declare []. Currently all network access is blocked for web wallpapers, even hosts listed here. Bundle everything. |
audio | bool | Reserved. Declare it honestly. Video always plays muted. |
nowPlaying | bool | Web only. When true the page receives the track playing in Music/Spotify (see below). |
Checksum algorithm (normative)
- Every regular file under
content/, path relative tocontent/, using/. - Sort by UTF-8 bytes, ascending.
- For each file, feed a SHA-256 with:
UTF-8 path+ one0x00byte +file bytes. - Result:
"sha256-" + lowercase hex.
import hashlib, os
def checksum(content_dir):
rels = sorted((os.path.relpath(os.path.join(r, f), content_dir).replace(os.sep, "/")
for r, _, fs in os.walk(content_dir) for f in fs if f != ".DS_Store"),
key=lambda p: p.encode())
h = hashlib.sha256()
for rel in rels:
h.update(rel.encode()); h.update(b"\0")
h.update(open(os.path.join(content_dir, rel), "rb").read())
return "sha256-" + h.hexdigest()
Careful: if the ZIP contains a .DS_Store inside content/, the app includes it in the hash.
Don’t ship one.
Metal contract
- A single
.metalfile of MSL source, compiled on the user’s Mac. - Required functions:
vertex … v_mainandfragment float4 f_main. - Uniforms at fragment
buffer(0), using exactly this struct (32 bytes):
struct Uniforms {
float2 resolution; // drawable pixels (Retina included)
float time; // seconds since start
float speed; // config "speed" (float), default 1.0
float4 tint; // config "tint" (color) as RGB 0..1, a = 1, default white
};
- There is no
frame,mouse, texture, or any otherconfigvalue in the shader. - The vertex stage draws 3 vertices (a full-screen triangle); use the guide’s
v_main. fragCoord.xyis in pixels, origin top-left.- Output is
bgra8Unorm, 8-bit, opaque. - Frame rate follows the display (ProMotion); the app throttles or pauses for battery/visibility.
Forbidden (textual check, case-sensitive, comments ignored):
| Text | Reason |
|---|---|
kernel / [[kernel | compute kernels are not allowed |
device | writable device buffers are not allowed |
threadgroup | threadgroup memory is not allowed |
atomic_ | atomic operations are not allowed |
#include " | local #include is not allowed (#include <metal_stdlib> is fine) |
Web environment
entrymust be an.htmlfile. Theentry’s folder becomes the root served atlwp://wallpaper/; only files inside it (and subfolders) are reachable.- Served with the right MIME type: html, js/mjs, css, json, png, jpg, gif, webp, svg, mp4/m4v,
wasm, woff/woff2, ttf. Anything else is
application/octet-stream. - Blocked: all network,
file://, navigating away, camera/microphone/location. - Storage is ephemeral. No mouse/keyboard input.
console.error/console.warnand uncaught errors go to the app log (see Debugging).- On import the app logs non-blocking warnings if it finds
fetch(,XMLHttpRequest,WebSocket,EventSource,importScripts,http://,https://,eval(,new Function(,atob(.
Page API:
window.LiveWallpaper.config // { key: value }, ready before your scripts run
window.LiveWallpaper.onConfig = cfg => { … } // called when the user changes settings
// Only with "capabilities": { "nowPlaying": true }
window.LiveWallpaper.onNowPlaying = np => { … }
// np = { isPlaying, title, artist, album, position, duration, source, artwork? }
// position/duration in seconds; artwork = data URI, sent only once per track; keep it.
Video
.mp4,.movor.m4v, decoded by macOS (HEVC/H.264). Gapless loop, muted, fills the screen (edges cropped). Noconfig.- Importing a bare video builds the package automatically (
idderived from the content; re-importing the same video replaces it instead of duplicating).
Limits
| Limit | Value |
|---|---|
| Size per file (uncompressed) | 256 MB |
| Files per package | 4096 |
| Paths | no .., no leading / |
Plans
| Free | Premium | |
|---|---|---|
Import/share metal and web wallpapers | ✅ | ✅ |
video wallpapers | ❌ | ✅ |
Errors
Exact messages as shown in the “Could not import wallpaper” alert, and how to fix each. The two “The data couldn’t be read…” messages come from macOS and may appear in your system language.
| Message | Cause / fix |
|---|---|
Not a valid .livewallpaper (ZIP) file. | Not a ZIP (or truncated). Rebuild with pack.py. |
Corrupt archive: unsafe path '…'. | A path contains .. or starts with /. |
Corrupt archive: unsupported compression method N. | Use store or deflate (the default for zip and pack.py). |
Corrupt archive: … (other) | Damaged ZIP. Rebuild it. |
Archive or an entry exceeds the size limit. | A file > 256 MB, or more than 4096 files. |
Package has no manifest.json. | manifest.json isn’t at the ZIP root. Zip the folder’s contents, not a folder with Finder junk (__MACOSX). |
The data couldn't be read because it is missing. | A required field is missing: schemaVersion, id, version, title, type, entry (or key/type in a config entry). |
The data couldn't be read because it isn't in the correct format. | Invalid JSON or wrong type: trailing comma, "schemaVersion": "1", "version": 1, type not metal/web/video, min as a string… |
Unsupported manifest schemaVersion N (expected 1). | Use "schemaVersion": 1. |
Manifest field 'id' is missing or empty. (or title, entry) | Fill in the field. |
Wallpaper requires macOS X, which is newer than this system. | Lower minMacOS (e.g. "26.0") or remove it. |
Package checksum does not match its content (possibly corrupt or tampered). | You changed content/ after computing the checksum, or the ZIP has a .DS_Store. Re-run pack.py. |
Package entry file '…' is missing. | entry doesn’t point to an existing file. It must include content/ and match case. |
Shader rejected: … . | A forbidden construct (Metal table above). Remove/rename it. |
Shader is missing the required 'f_main' fragment function. | Name the fragment function f_main. |
“.ext” isn't a supported video (use .mp4, .mov or .m4v). | Convert the video. |
That video is N MB — the limit is 256 MB. | Compress (HEVC) or shorten it. |
Silent failures (imports fine, but black/static):
- Metal fails to compile, or
v_mainis missing → black. Change a little at a time. typedoesn’t match the file → black.- Web: a JavaScript error, an asset outside the
entry’s folder, or anything fetched from the internet (blocked).
Debugging
Watch the app log in Terminal while the wallpaper runs:
log stream --level info --predicate 'subsystem == "com.livewallpaper.app"'
Look for Shader compile/pipeline failed (Metal compile error, with line number) and messages in
the WebConsole category (JavaScript errors).