- all text renders in the bundled Overpass SemiBold (Immich's typeface) via vita2d's FreeType path, with a per-pixel-size font instance cache (vita2d's glyph atlas caches glyphs at their first drawn size, which rendered stray oversized letters when sizes mixed) and a system-PGF fallback if the TTF fails to load; the OFL license ships in the vpk next to the font - freetype's bzip2 references get failing stubs (the SDK ships no libbz2 and plain TTFs never hit that path) - icon rebuilt from the official high-res Immich logo: anti-aliased, flattened onto white so the LiveArea no longer shows it on black Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vitaImmich
Minimal proof-of-concept Immich client for the PS Vita. It connects to your Immich server and shows your library (photos and videos) as a scrollable chronological grid (newest first), with a full-screen viewer and hardware-accelerated video playback.
Requirements
- A PS Vita with HENkaku/h-encore and VitaShell
- An Immich server reachable from the Vita's Wi-Fi (recent Immich version —
it uses
POST /api/search/randomandGET /api/assets/{id}/thumbnail) - An Immich API key (web UI → Account Settings → API Keys)
Building
Needs VitaSDK with the libvita2d, curl, openssl,
zstd, zlib, libpng, libjpeg-turbo and freetype packages installed
(via vdpm).
export VITASDK=$HOME/vitasdk
export PATH=$VITASDK/bin:$PATH
cmake -B build -S .
cmake --build build
This produces build/vitaImmich.vpk.
Installing & configuring
-
Copy
vitaImmich.vpkto the Vita and install it with VitaShell. -
Launch it once — it creates
ux0:data/vitaimmich/config.txt. -
Edit that file (VitaShell → SELECT for FTP makes this easy):
server=http://192.168.1.100:2283 apikey=your-immich-api-keyIf your server sits behind a public domain (reverse proxy) and your router does not support NAT loopback, add the server's LAN IP so the domain is pinned to it while on your home network:
server=https://immich.example.com apikey=your-immich-api-key serverip=192.168.1.100This keeps the Host header and TLS SNI on the domain (so the reverse proxy still routes correctly) but connects to the LAN IP directly.
-
Relaunch the app.
Controls
Grid view:
| Button | Action |
|---|---|
| D-pad | Move selection (hold to repeat) |
| L / R | Jump two rows up / down |
| X | Open item full screen |
| SELECT | Open the sync overview |
| START | Exit |
Full-screen view:
| Button | Action |
|---|---|
| Left / Right | Previous / next item |
| X | Play video / retry a failed photo |
| O | Back to grid |
| SELECT | Open the sync overview |
| START | Exit |
Sync overview (SELECT):
| Button | Action |
|---|---|
| X | Queue all local-only files to upload |
| O / SELECT | Back |
| START | Exit |
Video playback:
| Button | Action |
|---|---|
| X | Pause / resume |
| Left / Right | Seek ±10 seconds |
| O | Stop and go back |
More of the library is fetched automatically (100 photos at a time, newest
first) as you scroll toward the end. Errors are shown on screen and logged
to ux0:data/vitaimmich/log.txt.
Syncing the Vita's camera media
On startup the app mounts the Photos app's storage (photo0:, i.e. the
ACL-protected ux0:picture where camera shots and recordings live) via
sceAppMgrAppDataMount, scans it (plus ux0:video/CAMERA, a few levels
deep; .jpg/.jpeg/.png/.mp4) and merges those files into the same
date-sorted grid as your server library.
Files larger than 512 MB are skipped so a movie collection in ux0:video
is never hashed or offered for upload. Both knobs live in config.txt:
syncdir=<folder> (repeatable, replaces the default scan locations) and
syncmaxmb=<MB> (the size cap; 0 disables it).
A small badge in the top-left corner of each cell shows its status:
- blue-grey dot — cloud only (a server asset not present on the Vita)
- orange up-arrow — local only (on the Vita, not yet on the server)
- blinking orange — queued / uploading
- green check — backed up (exists on both; shown once, as the server asset)
- red dot — failed (see the sync overview for the reason)
A background thread hashes each local file (SHA1) and asks the server which
ones already exist (POST /api/assets/bulk-upload-check). Hashing and the
duplicate check start automatically. Uploads do not start on their own:
open the sync overview with SELECT and press X to queue every
local-only file. Each upload is a POST /api/assets multipart request with
an x-immich-checksum header for fast server-side dedup; progress is shown
in the overview.
PoC limitations
- TLS certificate verification is disabled (no CA bundle is shipped), so prefer plain HTTP on a trusted LAN or treat HTTPS as unverified.
- Thumbnails stream in on a background thread (one at a time over a keep-alive connection); cells show a grey placeholder until loaded.
- Videos are downloaded in full to
ux0:data/vitaimmich/video.mp4before playback (deleted afterwards), so they need free space on the memory card and a moment to start. Playback uses the Vita's hardware decoder via SceAvPlayer, so only MP4 (H.264/AAC) plays — Immich's transcodedvideo/playbackstream is used, which is H.264 with default server settings. The decoder rotates four output frame buffers (~13 MB CDRAM at 1080p) so async GPU rendering can't be torn by a recycled frame. - Caps at 1000 server assets and 500 local camera files per launch; no albums or search.
- Local media sync is one-way (Vita → server). It scans
ux0:pictureandux0:video/CAMERAby default (the standard camera locations), recursing ~2 levels; usesyncdir=in the config to scan other folders. - Local video grid cells show a poster thumbnail (with the VIDEO badge): the first frame is decoded with SceAvPlayer on the background loader thread (NV12 -> RGB, downscaled), the same handoff as photos. Poster extraction and playback share the single hardware decoder, so a poster is never decoded while a video is playing (and vice versa).
- Hashing 500 files at startup takes a while; it runs in the background so browsing is never blocked, but statuses fill in gradually.
deviceIdis hard-coded to "PS Vita" and uploads are not associated with an album.