Desktop app - Settings -> Picture: GPU passes between the stream and the screen. Upscaling (Bilinear, Lanczos, AMD FSR 1, FSRCNNX AI), CAS sharpening, denoise, debanding, dithering and colour; profiles plus a frame-time guard. My presets: save, apply, update, revert, delete, remembered. - Side-by-side compare (C) with zoom and pan, tweak panel (X). - One viewer: connecting takes the stream; "Watching elsewhere" when another viewer takes it. - Lower sound delay (about 190 -> 100 ms). - Gallery follows the folder, PNG sizes, Fit/100% switch, drag to pan. - Fixed crashes on fast zooming and on opening the gallery. Payload - 20 Mbit/s encoder bitrate; set once at start (live changes lock the encoder, documented in docs/API.md). - One viewer at a time (409 for a pushed-out viewer), viewers and bitrate in status.json. - Process name Glint.elf, previous log kept, quits at once when refused. Browser dashboard - Fixed the freeze after ~8 s, lower latency, sound on by default, one viewer with Watch here. Version 1.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.3 KiB
Glint payload HTTP API (port 7231)
The payload on the PS5 serves the console's H.264 encoder output, its audio and a set of controls over HTTP. The desktop app and the browser dashboard are both clients of this API.
Transport: plain HTTP/1.1 over TCP. Every response sets
Connection: close and Cache-Control: no-store (where applicable).
The request line is parsed as %11s %255s; only the method and path are used.
A request must be complete within 5 seconds (server-side receive timeout 1 s,
header deadline 5 s, 4095-byte limit).
Routes
| Method | Path | Response |
|---|---|---|
| GET | / |
text/html; charset=utf-8, the dashboard page |
| GET | /banner.png |
image/png, the banner image |
| GET | /config.json |
stream configuration |
| GET | /status.json |
live statistics |
| GET | /frames |
framed H.264 stream (endless) |
| GET | /audio |
framed PCM stream (endless) |
| GET | /rtp/video?port=N |
RTP/AVC stream to the caller's IP on UDP port N |
| GET | /recordings |
JSON array of recordings |
| GET | /recordings/<name> |
recording download |
| POST | /record/start |
arm recording on the next IDR |
| POST | /record/stop |
close and finalize the recording |
| POST | /video/keyframe |
request an IDR from the encoder |
| POST | /capture/profile?height=H |
change capture height |
| POST | /capture/restart |
restart the capture session |
| POST | /stop |
stop streaming and release capture |
Unknown routes return HTTP 200 with the body Unavailable route
(text/plain).
Video framing (/frames)
Each frame is:
offset size field
0 4 payload length, big-endian (max 2,097,152)
4 4 presentation timestamp, big-endian, 90 kHz units
8 N Annex-B access unit (00 00 00 01 / 00 00 01 start codes)
The first frame after a connection is a keyframe (the server requests one on
connect). After that keyframes come from the encoder's own schedule, after a
skip (below), or on POST /video/keyframe; the server does not force periodic
ones, because each large IDR is a latency spike on a slow link (Wi-Fi). The PC
recorder requests one every 2 s while it records. Timestamps are 32-bit and
wrap; the dashboard unwraps the delta.
Ring: 64 slots. When a viewer falls more than 6 frames behind the server drops
to the newest available IDR and requests another keyframe. Streaming clients
are capped at 8; the 9th gets HTTP/1.1 503 Busy with an empty body.
Audio framing (/audio)
offset size field
0 4 payload length, big-endian (max 65,536)
4 4 timestamp, big-endian (console clock)
8 N PCM samples
Codecs advertised in config.json:
pcm_f32le— 32-bit float, interleavedpcm_s16le— signed 16-bitpcm_s32le— signed 32-bit
Default console stream: pcm_f32le, 2 channels, 48000 Hz. The capture layer
emits one packet per audio frame batch, typically 8192 bytes (1024 stereo
float samples).
config.json
{
"platform": "PS5",
"width": 1920,
"height": 1080,
"fps": 60,
"codec": "avc1.4d4032",
"sps": "674d403291a01e0089f97016a020202800001f48000ea6044d0f1f50",
"pps": "68ee3c80",
"audio": {
"codec": "pcm_f32le",
"channels": 2,
"rate": 48000,
"bits": 32
},
"resolutions": [1080],
"requested_height": 1080,
"profile_changing": false,
"profile_error": false
}
codecisavc1.%02x%02x%02xfromsps[1..3](profile, constraints, level).sps/ppsare lowercase hex without start codes.audioisnullwhen no audio channel is open.- Glint captures 1080p60 only (capture feature 12). On current firmware
1440p
OpenVideoreturns0x81950008(profile unsupported), and a failed 1440p attempt can leave the encoder in a state whereStartreturns0x80c78000, so the payload opens 1080p directly without probing. Seedocs/CAPTURE-INTERNALS.mdfor the full error-code table.
status.json
{
"version": "1.5",
"backend": "h264",
"width": 1920,
"height": 1080,
"frames": 648,
"bytes": 1354059,
"fps": 60.3,
"kbps": 1008.8,
"clients": 1,
"viewers": 1,
"bitrate": 20000000,
"recording": false,
"recording_name": "",
"recording_frames": 0,
"recording_bytes": 0,
"recording_seconds": 0.0,
"recording_error": false,
"audio_chunks": 508,
"audio_bytes": 4161536,
"audio_kbps": 3108.5,
"audio_codec": "pcm_f32le"
}
fps and kbps are frames and bytes since the payload started divided by the
time since the server last (re)started, so they overcount after an encoder
reopen. The app shows its own rate over the last 2 s instead.
clients counts every open request socket (including status polling).
viewers is how many people are watching: 1 while the current viewer has a
stream open, else 0 (see One viewer at a time).
Recording
- Directory:
/data/glint/recordings(created on demand). - Trigger: the first IDR after
POST /record/start. - File name:
glint-<unix-time>-<frame-count>.h264. - Contents:
[BE u32 len][SPS] [BE u32 len][PPS]followed by Annex-B access units. A new SPS/PPS pair is written after a discontinuity. POST /record/stopfinalizes the file and fillsrecording_name,recording_frames,recording_bytes,recording_seconds.- A recording error sets
recording_errorand closes the file. - Listing entries:
{"name":"...","bytes":123}. - Download names are restricted to
[A-Za-z0-9.-], must end in.h264, must not start with., must not contain.., and are at most 96 chars.
Profile changes
POST /capture/profile?height=H accepts only 1080 (anything else is 400).
400 Unsupported resolution— bad height or trailing characters.409 Stop recording before changing resolution.409 A resolution change is already in progress.200 Resolution unchanged— same as current.200 Resolution change requested— capture loop reopens avcap2 and bumps the stream generation, which disconnects current viewers.
Response body shape: {"message":"..."} over HTTP/1.1 <code> Profile.
RTP
GET /rtp/video?port=N (accepted range 1024–65535):
- Sends
HTTP/1.1 200 OK ... RTP\n. - Opens a UDP socket to the caller's address with the requested port.
- Packetizes Annex-B NAL units as RTP/AVC (payload type 96, 90 kHz clock).
- SPS and PPS are prepended to every keyframe, each with a 4-byte start code.
- NALs larger than 1400 bytes use FU-A fragmentation; the marker bit is set on the last packet of each access unit.
Limits and concurrency
| Resource | Value |
|---|---|
| Client slots | 16 |
Streaming slots (/frames, /audio, /rtp) |
8 |
| Video ring | 64 entries |
| Audio ring | 256 entries |
| Request header | 4095 bytes |
| Header deadline | 5 s |
| Send timeout | 5 s |
| Receive timeout | 1 s |
| Max video frame | 2 MiB |
| Max audio packet | 64 KiB |
One viewer at a time
/frames, /audio and /rtp/video belong to one viewer at a time. A viewer
is named by ?viewer=<id> (letters, digits, -, _; the dashboard makes a
new one per page) or, without it, by the client's IP address (the desktop app).
- A different viewer connecting takes over: every stream of the current viewer ends.
- A viewer that was pushed out is refused with HTTP 409 while the one that pushed it is still watching (or reconnecting within 5 s), so two clients that reconnect on their own can't take turns kicking each other.
&take=1takes over anyway (the dashboard's Watch here button).- Once the current viewer has been gone for 5 s, anyone can connect again.
Bitrate
The encoder runs at 20 Mbit/s (bitrate in status.json; a bitrate=N
start argument changes it). Measured in a busy game over Wi-Fi, 20 Mbit/s
hitched about half as often as 30, and 40 and 60 more still; a calm home
screen needs far less.
It is set once, before Start, and can't change while Glint runs (tested on a
PS5, 13.60):
SetVideoBitrateon a running session returns ok, but everyReadVideoafter it fails with 0x80c78000 and the encoder stays locked until a restart.- Closing the session cleanly (Stop, Close, arbitrator cleared) and opening a
new one gets through
OpenVideoandSetVideoBitrate, thenStartis refused with 0x80c78000. The encoder takes one capture session per boot. - Every frame is a reference frame (
nal_ref_idc2), so a cap after the encoder can't drop frames without breaking the picture.