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

FieldTypeReq.Notes
schemaVersionnumberyesAlways 1.
idstringyesNon-empty. Stable identity; importing another package with the same id replaces the installed one. Use reverse-DNS (com.yourname.name) or a UUID.
versionstringyese.g. "1.0.0". Must be a string (not a number).
titlestringyesNon-empty. Shown in the app.
authorobjectno{ "id": string?, "handle": string? }.
typestringyes"metal", "web" or "video".
entrystringyesPath of the main file from the package root, e.g. content/shader.metal. Must exist.
minMacOSstringnoe.g. "26.0". On an older Mac the import fails.
checksumstringrecommendedsha256-… over content/ (algorithm below). If present it must match, on import and on every load.
configarraynoUser settings (table below).
capabilitiesobjectnoTable 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[]

FieldTypeNotes
keystringRequired. Name of the value delivered to the wallpaper.
typestringfloat, int, bool, color. Anything else (including enum) is ignored.
labelstringControl label. Default: key.
min, maxnumberfloat/int. Defaults 0 and 1.
defaultnumber / bool / stringfloat/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

FieldTypeNotes
networkarray of stringsDeclare []. Currently all network access is blocked for web wallpapers, even hosts listed here. Bundle everything.
audioboolReserved. Declare it honestly. Video always plays muted.
nowPlayingboolWeb only. When true the page receives the track playing in Music/Spotify (see below).

Checksum algorithm (normative)

  1. Every regular file under content/, path relative to content/, using /.
  2. Sort by UTF-8 bytes, ascending.
  3. For each file, feed a SHA-256 with: UTF-8 path + one 0x00 byte + file bytes.
  4. 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

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
};

Forbidden (textual check, case-sensitive, comments ignored):

TextReason
kernel / [[kernelcompute 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

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

Limits

LimitValue
Size per file (uncompressed)256 MB
Files per package4096
Pathsno .., no leading /

Plans

FreePremium
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.

MessageCause / 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):

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).