Image Stream Protocol — Reverse Engineering Notes
Note
This page documents the reverse engineering of the Seestar’s live image streaming protocol, carried out in February 2026. It is written to serve both as a human reference and as context for future AI-assisted development.
Background
The Seestar Android app displays a progressively-improving stacked image during observation. This is not the RTSP video feed (which is the raw camera viewfinder) — it is a separate binary socket that delivers the actual stacked deep-sky image.
Understanding this protocol was needed to build the stream module in
seestarpy, which lets users grab live stacked images in Python.
Discovery: dual streaming architecture
The Seestar exposes five TCP ports, each serving a different purpose:
Port |
Protocol |
Purpose |
|---|---|---|
4700 |
JSON-RPC |
Command and control (already implemented in seestarpy) |
4554 |
RTSP |
Live H.264 video — telephoto camera (viewfinder) |
4555 |
RTSP |
Live H.264 video — wide-angle camera |
4800 |
Binary |
Live stacked images — telephoto camera |
4804 |
Binary |
Live stacked images — wide-angle camera |
The RTSP streams are standard H.264 video (rtsp://<ip>:<port>/stream)
decoded via FFmpeg in the app. They show the raw camera feed and are useful
for aiming and focusing but do not show the stacked image.
Ports 4800 and 4804 are the interesting ones — they use a custom binary framing protocol to deliver the progressively stacked image.
Methodology
The protocol was reverse-engineered in two phases:
Static analysis — The Seestar Android APK (v3.0.2) was decompiled with JADX. The relevant Java classes were found under
com.wss.rxscoketclient(socket I/O) andcom.zwo.seestar.socket(connection management).Live capture — The protocol was confirmed by connecting to three live Seestar S50 telescopes and exchanging real data on port 4800.
Binary frame protocol
Each frame on port 4800/4804 consists of a 34-byte header followed by an image payload.
Header format (34 bytes, big-endian)
Offset |
Size |
Field |
Notes |
|---|---|---|---|
0-1 |
uint16 |
magic |
Must be |
2-3 |
uint16 |
version |
Protocol version (observed: 2) |
4-5 |
uint16 |
(gap) |
Unused by the app’s header parser |
6-9 |
uint32 |
length |
Image payload size in bytes (big-endian) |
10-11 |
uint16 |
(gap) |
Unused |
12 |
byte |
is_big_endian |
Endianness flag |
13 |
byte |
img_type |
1 = preview, 5 = stacked |
14 |
byte |
data_type |
Data format identifier (observed: 3) |
15 |
byte |
frame_id |
Frame identifier |
16-17 |
uint16 |
width |
Image width in pixels |
18-19 |
uint16 |
height |
Image height in pixels |
20-21 |
uint16 |
hfd_x |
Half-flux-diameter X |
22-23 |
uint16 |
hfd_y |
Half-flux-diameter Y |
24-25 |
uint16 |
hfd |
Half-flux-diameter value (focus quality) |
26-27 |
uint16 |
can_debayer |
Bayer pattern flag (1 = raw Bayer input) |
28-29 |
uint16 |
image_id |
Sequential frame counter |
30-31 |
uint16 |
reserved5 |
Unused |
32-33 |
uint16 |
reserved6 |
Unused |
The magic number constant is defined as FileDealUtil.MAGIC_NUMBER = 963
in the APK source (com.wss.rxscoketclient.deal.FileDealUtil).
Image payload formats
The payload format depends on the frame type. There are three kinds of
frame, distinguished by img_type and data_type in the header:
Stacked frames (img_type=5, data_type=3)
Returned by get_stacked_img and during active observation. The payload
is a streaming ZIP archive containing deflate-compressed raw pixel data.
Structure:
[zero padding] [ZIP local file header] [deflate stream]
Specifically:
A variable number of zero bytes (observed: 46 bytes)
A ZIP local-file header (
PK\x03\x04) with:Version 4.5 (ZIP64)
Compression method: deflate (8)
Bit flag 0x0008 (data descriptor, streaming — sizes not in header)
Filename:
raw_data
The deflate-compressed pixel data
The decompressed data is 16-bit RGB (48 bits per pixel):
height × width × 3 × sizeof(uint16) bytes
For a 4K S50: 3840 × 2160 × 3 × 2 = 49,766,400 bytes (decompressed from ~24 MB compressed).
This can be loaded directly into a NumPy array:
np.frombuffer(raw, dtype=np.uint16).reshape((height, width, 3))
Preview frames (img_type=1, data_type=2)
Pushed by begin_streaming (the live camera feed). The payload is raw
uncompressed 16-bit Bayer data — a single channel before demosaicing:
height × width × sizeof(uint16) bytes
For a standard S50: 1920 × 1080 × 2 = 4,147,200 bytes. The
can_debayer header field is set to 1. Load as:
np.frombuffer(payload, dtype=np.uint16).reshape((height, width))
Ack / keepalive frames (img_type=0)
Sent periodically with width=0, height=0, and a tiny payload (4–17
bytes). These should be silently skipped.
Socket multiplexing
A critical implementation detail: the image socket carries both binary
frames and JSON-RPC text responses on the same TCP connection. Heartbeat
responses (test_connection acks) arrive as \r\n-terminated JSON
between binary frames.
The reader must synchronise on the magic number 0x03C3 and skip any
interleaved JSON lines (which start with {). See
stream._read_frame() for the implementation.
JSON-RPC commands (same socket)
The image socket also accepts JSON-RPC commands, sent as
\\r\\n-terminated JSON strings:
{"method": "test_connection", "id": 1}
{"method": "begin_streaming", "id": 2}
{"method": "stop_streaming", "id": 2}
{"method": "get_current_img", "id": 2}
{"method": "get_stacked_img", "id": 2}
test_connection— Heartbeat, must be sent every ~4 seconds to keep the connection alive.begin_streaming— Start continuous frame delivery.stop_streaming— Stop continuous frame delivery.get_stacked_img— Request a single stacked image frame.get_current_img— Request a single preview frame.
Connection lifecycle
The official app follows this pattern:
Open TCP socket to port 4800 with
TCP_NODELAY, 64 KB buffers, keepaliveStart a heartbeat thread sending
test_connectionevery 4000 msSend
begin_streaming(orget_stacked_imgfor one-shot)Read loop: 34-byte header → validate magic → read
lengthbytes → processSend
stop_streaming→ close socket
Observed resolutions
Tested against three Seestar S50 units on the same network:
Seestar |
Resolution |
image_id |
|---|---|---|
seestar.local |
2160 × 3840 |
98 |
seestar-2.local |
1080 × 1920 |
30 |
seestar-3.local |
2160 × 3840 |
237 |
The 2160 × 3840 resolution comes from the S50’s “enhanced” mode, which uses sub-pixel dithering to populate a 4K grid from the native 1080p sensor. seestar-2 was not in enhanced mode at the time of capture.
Key APK source files
All paths relative to the decompiled APK (sources/):
com/wss/rxscoketclient/SocketObservable.java— TCP socket + ReadThread (binary frame reader; therun()method was too complex for JADX to decompile — 1285 bytecode instructions)com/wss/rxscoketclient/deal/HeaderData.java— 34-byte header parsercom/wss/rxscoketclient/deal/FileDealUtil.java—MAGIC_NUMBER = 963,bytes2int()helpercom/wss/rxscoketclient/deal/ImageEvent.java— Image metadata DTOcom/zwo/seestar/socket/MainFileManager.java— JSON-RPC command senderscom/zwo/seestar/socket/SocketManager.java— Connection lifecycle, heartbeat, retry logiccom/zwo/kit/utils/RtspUtilsKt.java— RTSP URL builder
Data flow in the app
Telescope (port 4800)
│
▼
SocketObservable.ReadThread.run()
│ 1. Read 34-byte header
│ 2. Validate magic == 0x03C3
│ 3. Read `length` bytes of image payload
│ 4. Write payload to temp file on disk
│ 5. Create ImageEvent(header, filePath, fileSize)
│
▼
SocketObservable.SocketObserver.onNext(ImageEvent)
│ Wrap in DataWrapper(state=1, data=imageEvent)
│
▼
SocketManager.onResponse(ImageEvent)
│ Post to EventBus for UI subscribers
│
▼
UI fragments load image from imageEvent.getPath()
What’s left to explore
Wide camera stream (port 4804) — Confirmed to accept the same protocol but not yet tested with live data.
Dual-pane live display — During
begin_streaming, the telescope pushes preview frames (single subs) and the heartbeat can poll stacked frames. A side-by-side matplotlib display (preview | stack) would let the user spot clouds in new subs while watching the stack improve.Continuous streaming during active observation — Does the telescope push stacked frames (img_type=5) automatically via
begin_streamingduring observation, or only preview frames? The exact cadence and frame types during active stacking need measurement.RTSP integration — The RTSP ports (4554/4555) use standard H.264. An OpenCV or ffmpeg-based live preview could complement the stacked-image stream.