Stream your media library from a PC to a real PSP over WiFi.
The PC transcodes a file with ffmpeg into a PSP Movie Format (PSMF/PMF)
container, caches it, and serves it over HTTP. A PSP homebrew client connects
over WiFi, pulls the stream into the Media Engine ringbuffer, and plays it with
sceMpeg (H.264) while rendering through the GU via
oslib.
Why PMF and not MP4?
sceMpeg— the only user-mode video decoder on the PSP — rejects plain MP4 and requires the SonyPSMFcontainer (an 0x800-byte header followed by MPEG-2 packs). Seeinternal/pmf.
media file ──ffmpeg──▶ raw H.264 ES ─┐
├──▶ PSMF muxer (Go) ──▶ .pmf ──HTTP──▶ PSP
media file ──atracdenc──▶ ATRAC3+ ───┘
- Video — ffmpeg decodes the source, scales/pads it to 480x272, forces CFR 29.97, and encodes H.264 Main @ level 3.0 into a raw Annex-B elementary stream.
- Audio — ffmpeg extracts the first audio track as 44.1 kHz stereo PCM, and
atracdencencodes it to raw ATRAC3+ frames (ffmpeg cannot encode ATRAC3+). - Mux —
internal/pmfsplits the H.264 into access units, builds the PSMF header, and interleaves video PES (0xE0) and audio PES (0xBD) in timestamp order. - Cache — the PMF is cached on disk. The Media Engine needs the full header and total stream size up front, so the whole file is transcoded before playback rather than fragmented live.
- Seek — a
?start=offset is served by re-muxing the cached elementary streams from the nearest IDR; no re-encode, so seeks are near-instant. - Prefetch — opening a show queues its episodes for background transcoding (one at a time), so by the time you press play they are usually already cached.
| Path | What |
|---|---|
cmd/psprelay/ |
PC server entrypoint (Go) |
internal/library/ |
Media scan + catalog (movies and shows/seasons/episodes) |
internal/transcode/ |
ffmpeg + atracdenc pipeline, cache, prefetch |
internal/pmf/ |
PSMF muxer (Go) |
internal/server/ |
HTTP + UDP discovery |
psp/ |
PSP homebrew client (C, pspdev SDK) → psp/EBOOT.PBP |
tools/ |
Build/run helpers |
PC server (Go, standard library only):
go build -o psprelay ./cmd/psprelayRequires ffmpeg/ffprobe on PATH. Audio needs an ATRAC3+ encoder —
we use atracdenc, patched to allow
retail bitrates (128/192/256/320 kbit/s). Without it the server produces
video-only PMFs; point -atracdenc at the binary, or pass -atracdenc "" to
disable audio.
PSP client (C, pspdev SDK, built via Docker using the pspdev/pspdev image):
tools/pspbuild.sh # -> psp/EBOOT.PBP
tools/pspbuild.sh clean./psprelay -library ~/Videos -cache ./cache -port 8770 \
-atracdenc ~/.local/bin/atracdenc -atrac-bitrate 128Then open http://<pc-ip>:8770/ in a browser, or point the PSP client at it.
Useful flags: -encoder h264_nvenc|h264_vaapi|h264_qsv, -vbitrate 768k,
-preset veryfast, -width 480 -height 272, -force.
The host firewall must allow inbound TCP 8770 and UDP 8771. On a default-deny setup, a blocked port makes the client hang on "loading library" rather than error (the client has no connect timeout yet).
| Route | Purpose |
|---|---|
GET / |
HTML catalog |
GET /api/media |
JSON catalog |
GET /list |
showId<TAB>episodeCount<TAB>title lines |
GET /show/{id} |
season<TAB>seasonTitle<TAB>episodeId<TAB>episodeName lines |
GET /thumb/{id} |
JPEG still (browser) |
GET /raw/{id} |
Raw RGBA8888 still (PSP client) |
GET /prepare/{id} |
Start/peek a transcode |
GET /status/{id} |
status percent [etaSeconds] |
GET /cancel/{id} |
Abort an in-flight transcode |
GET /stream/{id} |
The cached PMF |
/prepare, /status, /cancel and /stream accept ?start=<seconds>.
The server also answers UDP discovery probes (PSPRELAY-DISCOVER → PSPRELAY <port>) on port 8771 so the client can find it automatically.
Copy EBOOT.PBP to the PSP (or PPSSPP's PSP/GAME/PSPRelay/). On first launch
a Settings screen asks for the WiFi profile, server address and port (or
press Discover server). Settings persist to ms0:/PSPRelay/config.txt.
- Browse — a poster grid of shows. X opens a show, which lists its episodes grouped by season. X plays. O goes back, SELECT settings, HOME quits.
- Player — X/START pause, ←/→ ±10 s, L/R ±60 s, ↑/↓ volume, O back to the menu.
Thumbnails and posters are decoded server-side to raw RGBA, so the console needs no image decoder.
Working
- End-to-end streaming of H.264 video + ATRAC3+ audio from a PC to a real PSP-2000 (ARK-5, 6.61) and to PPSSPP.
- Movie library and TV library (show → season → episode), poster/thumbnail grid UI, settings screen, in-player seeking, background prefetch.
- Cached content starts instantly and plays correctly.
Limitations
- Transcoding is the bottleneck. On a 2-core i7-6600U, 1080p H.264 software decode runs at roughly 2.5–3x realtime end to end, so a 22-minute episode takes several minutes to prepare. Prefetch hides this in normal use, but anything not yet cached is slow to start. Hardware decode was measured and did not help here (frame download cost exceeded the decode saving).
- The player leaks its ringbuffer/MPEG/audio buffers on every seek. They are deliberately not freed on close to dodge a heap-corruption bug in teardown. Long sessions with many seeks will exhaust memory; fix the teardown and re-enable the frees.
- Playback speed / audio-sync anomaly on some freshly transcoded TV episodes. Cached content plays correctly, so the player and muxer are believed sound; the cause is not yet root-caused.
- No connect timeout in the client. If the server is unreachable (e.g. the firewall drops the SYN), the UI hangs instead of reporting an error.
sceMpegonly accepts the SonyPSMFcontainer, not MP4.- AV modules must be loaded with
sceUtilityLoadAvModule(PSP_AV_MODULE_*), notsceUtilityLoadModule(PSP_MODULE_AV_*). sceMpegAvcDecodetakes the output buffer by double indirection (&buffer), even though the SDK header declares a plain pointer. Passing the pointer directly makes it returnSCE_MPEG_ERROR_INVALID_VALUE(0x806101FE).- The decoded frame buffer belongs in eDRAM, after the GU draw/display buffers.
- PSMF audio is ATRAC3+ in private-stream-1 PES (
0xBD), one 8-byte PSMF frame header per codec frame. - The client plays audio on its own thread — feeding
sceAudioOutputBlockingfrom the video loop starves the channel and the sound flutters. - The Media Engine writes RGBA with alpha 0; oslib must draw the video with
oslSetAlpha(OSL_FX_NONE, 0)or the frame is fully transparent. - Draw the video through oslib (
oslDrawImage), not raw GU: oslib caches its current texture, so raw GU texture changes make the overlay text sample the video texture and render as garbage. - Create the video decode image once for the app's lifetime. Repeatedly creating and deleting a large VRAM image corrupts oslib's image list.
- Terminate the audio thread (
sceKernelTerminateDeleteThread) rather thansceKernelWaitThreadEnd, which can hang while the thread is blocked insceAudioOutputBlocking.