Skip to content

feat: Jieli HF toy drones over CTP (#187) - #198

Merged
compot-dev merged 3 commits into
feat/flowufo-187from
feat/jieli-187
Oct 15, 2026
Merged

compot-dev merged 3 commits into
feat/flowufo-187from
feat/jieli-187

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Refs #187, #196. Into feat/flowufo-187 (PR #188), to be merged there later. Draft until a device test on an HF_… toy.

What it does

A new slot type jieli:// for toy drones with an HF_xxxxxxxx Wi‑Fi (Jieli AC79 camera, vendor app DVRunning 2), in single view and grid cells like the other toy slots. Video only: FLIGHT_CONTROL is never sent, nothing goes to the toy's UDP 3456.

  • CTP on TCP 3333 (default 192.168.80.1:3333): CTP: + topic length u16 LE + topic + JSON length u32 LE + JSON (may be empty). A stream reader reassembles split/joined messages and skips bytes that are not CTP.
  • Session, as in the PCAPdroid capture of the vendor app (Reference: open-source work on Wi‑Fi toy drones (cooingdv / KY UFO and others) #196): APP_ACCESS {"ver":"25","type":"0"} → (waits up to 1.5 s for VIDEO_PARAM) → DATE_TIME (phone time, yyyyMMddHHmmss) → CTP_KEEP_ALIVE every min(announced timeout / 4, 7.5 s) → OPEN_RT_STREAM {"format":"0","h":…,"w":…,"fps":…} with the size from VIDEO_PARAM (640×480@25 if none). CLOSE_RT_STREAM {"status":"1"} when the slot stops (also on RtspSession.stop(): the session thread sends it before closing). CTP silence beyond max(20 s, announced timeout) or a closed connection reconnects through the normal back-off.
  • Video (unverified): listens on the phone's UDP 6666 (bound before OPEN_RT_STREAM, socket bound to the toy's network). RTP PT 26 → existing RTP/JPEG path (JpegAssembler, RFC 2435); PT 96 → existing H.264 RTP path (as on udp:// slots; the slot follows the PT the toy actually sends); a whole JPEG in a datagram without RTP → MJPEG frame path. Only packets from the toy's address are played; others are counted and logged.
  • Diagnostics: the first 8 packets on 6666 are logged with sender addr:port, RTP PT/seq/marker or "not RTP", and the first 32 bytes in hex. No video 5 s after OPEN_RT_STREAM → log + status No video on UDP 6666 yet…, and UDP 1234 (SDK audio port) and 2224 (SDK RT stream port) are opened for the log only (first 5 packets each, counts in stats). OPEN_RT_STREAM is repeated every 10 s without video.
  • URL: jieli://[host][:ctp-port][?w=&h=&fps=&rtp=&format=0|1], defaults 192.168.80.1:3333, rtp 6666, format 0.
  • Status texts: Connecting to the toy (CTP 192.168.80.1:3333), Stream requested, waiting for video on UDP 6666, No video on UDP 6666 yet: the toy accepted the request; see /log, Receiving from the toy on UDP 6666…, The toy's video stopped: waiting on UDP 6666.
  • /stats.json: jieli object per cell and in single: state, target, network, hint, ctp (sent/received, keepalives/replies, keepalive_ms, announced_timeout_ms, open_requests, topics counts, last_notifies incl. VIDEO_PARAM), video_param, requested, rtp (port, network, packets, bytes, video_packets, frames (marker bits), payload_types, not_rtp, from_others, first_packet), sources, diagnostic_ports, last_error. CTP messages are logged (first 60, then every 100th; keepalives only the first two; JSON cut at 200 chars).
  • Subnet hint: a jieli:// slot on a 192.168.169.x / 192.168.1.x phone suggests flowufo:// / the KY UFO RTSP URL; a flowufo:// slot on a 192.168.80.x phone now suggests jieli://192.168.80.1 (instead of "not supported yet").
  • NetInfo.bindToNetworkOf(Socket, …) for the CTP socket (Android 6+), sharing the network lookup with the UDP variant.
  • Single-view stream checks skip jieli:// slots; transport label "Jieli RTP".
  • docs/toys.md: HF / Jieli row and section (test build 3, video path unverified), credits to turbodrone + Reference: open-source work on Wi‑Fi toy drones (cooingdv / KY UFO and others) #196; transports.md, debug.md.
  • No version bump (the feat: FLOW-UFO toy drones over UDP (#187) #188 line gets one when this is merged).

Tests

JieliTest: CTP encode (exact bytes of CTP_KEEP_ALIVE {"op":"PUT"}, empty JSON), reader byte-by-byte / joined / split across a length field / garbage and bad lengths / a large message, JSON reader, keepalive interval, URL options and errors, subnet hints, and a fake CTP toy on loopback: APP_ACCESS → DATE_TIME → CTP_KEEP_ALIVE → OPEN_RT_STREAM with the VIDEO_PARAM size, keepalives 400–900 ms apart at timeout 2000, RTP/JPEG on a loopback UDP port producing the expected JPEG byte for byte, CLOSE_RT_STREAM on stop and on close(), 640×480@25 without VIDEO_PARAM, no-video diagnostics port + repeated OPEN_RT_STREAM, unreachable toy.

Not verified (no HF toy here)

  • That the video really goes to UDP 6666 as RTP (PT 26 or 96) — not in the capture; from turbodrone's notes on the same SDK.
  • Whether the toy sends from 192.168.80.1 (other senders are only logged).
  • Whether OPEN_RT_STREAM needs anything else first (e.g. the vendor app's UDP 3456 00 packets, which we never send) or a local SDP server (TCP 6789 in the SDK; not offered).
  • RTP/JPEG details of this firmware (type, in-band tables, restart markers) — the JpegAssembler handles RFC 2435 types 0/1 (+64 with DRI).
  • Network binding of the TCP socket on the tester's phone.

What a tester should send

App version/hash, phone and Android version; with the slot jieli://192.168.80.1 trying for ~30 s: http://127.0.0.1:8085/stats.json (the jieli object) and http://127.0.0.1:8085/log (lines with Jieli).

jieli://192.168.80.1 for toys with an HF_xxxxxxxx Wi-Fi (Jieli AC79, vendor app
DVRunning 2), after the PCAPdroid capture in #196 and turbodrone's research notes
(Apache-2.0, behaviour reference only, no code taken over):

- CTP on TCP 3333 (CTP: + u16 topic length + topic + u32 JSON length + JSON), a reader
  that reassembles split and joined messages and skips what is not CTP
- APP_ACCESS, DATE_TIME, CTP_KEEP_ALIVE every min(timeout / 4, 7.5 s), OPEN_RT_STREAM
  with the size of VIDEO_PARAM (else 640x480 at 25), CLOSE_RT_STREAM on stop
- video expected on the phone's UDP 6666 (unverified): PT 26 into the RTP/JPEG path,
  PT 96 into the H.264 path, whole JPEG datagrams too; first packets logged with
  sender, PT and bytes; no video for 5 s: said in the status, UDP 1234 and 2224 opened
  for the log only, OPEN_RT_STREAM again every 10 s
- options w, h, fps, rtp, format; sockets bound to the toy's network (TCP from Android 6)
- jieli object in /stats.json; subnet hints both ways between flowufo:// and jieli://
- never FLIGHT_CONTROL, nothing to UDP 3456
- docs/toys.md: HF / Jieli section, credits; unit tests with a fake toy on loopback
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Bad packets keep the toy marked as live 🐞 Bug ≡ Correctness ⭐ New
Description
video() updates lastVideoAt for any toy-origin packet with RTP version bits and payload type 26
or 96 before the decoder decides whether the packet is usable. If the toy repeatedly sends malformed
RTP, the assemblers reject it while the slot reports receiving and neither its quiet-state logic nor
its stream-reopen logic runs.
Code

android/app/src/main/java/com/openipc/decoder/Jieli.java[R1012-1013]

+                lastVideoAt = now();
+                listener.onRtp(b, 0, len);
Evidence
A 12-byte RTP packet with PT 26 passes Jieli's classification and refreshes its timestamp, but lacks
the eight-byte JPEG payload header required by the assembler. That timestamp drives both the
receiving state and the decision to repeat OPEN_RT_STREAM.

android/app/src/main/java/com/openipc/decoder/Jieli.java[981-983]
android/app/src/main/java/com/openipc/decoder/Jieli.java[1005-1013]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[110-128]
android/app/src/main/java/com/openipc/decoder/Jieli.java[757-799]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Malformed RTP refreshes Jieli video liveness even when the decoder rejects it, preventing quiet-state reporting and stream reopen.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[1005-1019]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[784-809]
## Recommended Fix
Have the media listener report whether the downstream callback accepted a packet or JPEG frame. Refresh `lastVideoAt` only for accepted media; retain separate counters for packets that arrived but were unusable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Silent toy slots block other UDP streams 🐞 Bug ≡ Correctness ⭐ New
Description
Client.run() opens diagnostic listeners on UDP 1234 and 2224 after five seconds without video and
keeps their sockets open until the Jieli session ends. In a grid, another cell configured to listen
on either port cannot bind its unicast socket for as long as the silent toy slot remains active.
Code

android/app/src/main/java/com/openipc/decoder/Jieli.java[R780-784]

+                            for (int p : diagnosticPorts) {
+                                if (p == url.rtpPort) continue;
+                                try {
+                                    startThread(openUdp(p, a), false, listener);
+                                } catch (IOException e) {
Evidence
The no-video branch binds both diagnostic ports, the diagnostic threads receive until their sockets
close, and Jieli otherwise closes tracked sockets only on session shutdown. The existing unicast UDP
listener binds with address reuse disabled, so a competing grid cell cannot claim one of those
ports.

android/app/src/main/java/com/openipc/decoder/Jieli.java[774-789]
android/app/src/main/java/com/openipc/decoder/Jieli.java[846-887]
android/app/src/main/java/com/openipc/decoder/Jieli.java[821-835]
android/app/src/main/java/com/openipc/decoder/RtpListener.java[170-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Diagnostic UDP listeners retain ports used by other grid streams for the entire silent Jieli session.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[774-789]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[846-887]
## Recommended Fix
Give diagnostic listeners a bounded observation period or packet limit, then close their sockets while retaining the collected counts and first-packet details in stats.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Toy messages can exhaust phone memory 🐞 Bug ☼ Reliability ⭐ New
Description
handle() adds every distinct CTP topic to topics without a size limit, although only individual
message lengths are bounded. A peer sending a continuing series of valid messages with different
topic names grows both the retained map and the copy made for each /stats.json request without
bound.
Code

android/app/src/main/java/com/openipc/decoder/Jieli.java[R932-933]

+                Integer c = topics.get(m.topic);
+                topics.put(m.topic, c == null ? 1 : c + 1);
Evidence
The reader permits arbitrary topic names within its per-message length limit; every resulting
message reaches handle(), which retains each new name. Stats copies the complete retained map.

android/app/src/main/java/com/openipc/decoder/Jieli.java[276-295]
android/app/src/main/java/com/openipc/decoder/Jieli.java[813-815]
android/app/src/main/java/com/openipc/decoder/Jieli.java[929-937]
android/app/src/main/java/com/openipc/decoder/Jieli.java[1075-1079]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A connected CTP peer can cause unlimited growth in the topic statistics map by sending distinct valid topics.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[929-937]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[1075-1079]
## Recommended Fix
Cap the number of distinct topics retained and aggregate further unseen topics into an overflow count, so stats snapshots remain bounded too.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
4. Toy reconnects wait up to 30 s after a dropout 🐞 Bug ☼ Reliability ⭐ New
Description
Jieli.Client.run() only returns normally when the slot is stopped. A closed CTP connection or CTP
silence ends it with an IOException instead, so jieli() never reaches return true and
networkLoop never sets played, which means the back-off is never reset. If the toy was off for a
few attempts at first (the delay doubles up to the 30 s maximum) and then streamed fine, every later
dropout still waits about 30 s before reconnecting, and each one counts as another failure through
onFailure, even though the comment says the method behaves "like a PLAY that succeeded".
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R817-818]

+            });
+            return true;
Evidence
networkLoop resets retryMs/failures only when connect() returns true; when it throws, it counts a
failure and keeps doubling the retry delay. Jieli.Client.run() leaves its while loop only when
closed or keepGoing fails (both mean stop). Every other exit is a throw: 'The toy closed the CTP
connection', 'The toy stopped answering on CTP', and the catch rethrows unless closed. So the
return true after c.run() runs only when the slot is stopped, and at that point gen != generation,
so the loop exits anyway.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[240-271]
android/app/src/main/java/com/openipc/decoder/Jieli.java[740-821]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[738-746]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A jieli:// session that connected and requested the stream still ends with an IOException when the toy drops the connection. Because of that, RtspSession.networkLoop never sees `played == true` and never resets the retry back-off (up to 30 s).

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[756-827]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[690-838]

## Recommended Fix
Add a flag on Jieli.Client, e.g. `volatile boolean streamRequested`, and set it when OPEN_RT_STREAM is first sent. In RtspSession.jieli(), wrap `c.run(...)` in a try/catch on IOException. If `c.streamRequested` is true, log the error (and call callback.onFailure if you want), then return true so the back-off resets. Otherwise rethrow.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Late timeout notices miss toy deadlines ✓ Resolved
Description
handle() updates keepAliveIntervalMs when KEEP_ALIVE_INTERVAL arrives, but leaves the already
scheduled nextKeepAlive unchanged. If the notice arrives after the first keepalive with a timeout
shorter than the initial 7.5-second interval, the next keepalive still waits on the old schedule.
Code

android/app/src/main/java/com/openipc/decoder/Jieli.java[R941-942]

+                    announcedTimeoutMs = timeout;
+                    keepAliveIntervalMs = keepAliveMs(timeout);
Evidence
The first keepalive uses the default interval to set a local deadline; the receive handler changes
only the interval field, while the loop continues comparing against that unchanged deadline.

android/app/src/main/java/com/openipc/decoder/Jieli.java[725-747]
android/app/src/main/java/com/openipc/decoder/Jieli.java[937-945]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A timeout announcement received after the first keepalive changes the interval but not the pending deadline, so the next keepalive can be too late.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[735-747]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[938-945]
## Recommended Fix
Track the last keepalive send time and recompute the next deadline when an announcement changes the interval. Send immediately if the new deadline has already passed, and test an announcement received after the initial keepalive.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A live toy stream can show as stopped ✓ Resolved
Description
onState(false, ...) clears the transport label and publishes a stopped status without ordering
that update against usable() on the separate UDP thread. When a packet arrives as the CTP thread
declares the stream quiet, the negative status can run after that packet reports connected and
remain visible until another usable packet arrives.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R770-773]

+                        inUse = null;
+                        // Video that stopped says so; the next frame reports "connected" again
+                        status(gen, received.get() ? "disconnected" : "buffering");
+                        first.set(true);
Evidence
The CTP loop bases its quiet transition on a snapshot of lastVideoAt; a separate UDP receiver
invokes the packet callback, whose usable() method can publish connected before the CTP callback
publishes disconnected.

android/app/src/main/java/com/openipc/decoder/Jieli.java[749-765]
android/app/src/main/java/com/openipc/decoder/Jieli.java[859-879]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[764-808]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CTP thread can publish a quiet status after a concurrent UDP packet has restored the connected status.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[749-765]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[764-808]
## Recommended Fix
Serialize state and usable-packet updates, or recheck the latest video timestamp before publishing a negative status, so an older quiet observation cannot override a newer usable packet.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: 🧠 Deep: Large new protocol, networking, concurrency, and media logic spans many independent paths.

Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit d094871 🧠 Deep

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Late timeout notices miss toy deadlines ✓ Resolved
Description
handle() updates keepAliveIntervalMs when KEEP_ALIVE_INTERVAL arrives, but leaves the already
scheduled nextKeepAlive unchanged. If the notice arrives after the first keepalive with a timeout
shorter than the initial 7.5-second interval, the next keepalive still waits on the old schedule.
Code

android/app/src/main/java/com/openipc/decoder/Jieli.java[R941-942]

+                    announcedTimeoutMs = timeout;
+                    keepAliveIntervalMs = keepAliveMs(timeout);
Evidence
The first keepalive uses the default interval to set a local deadline; the receive handler changes
only the interval field, while the loop continues comparing against that unchanged deadline.

android/app/src/main/java/com/openipc/decoder/Jieli.java[725-747]
android/app/src/main/java/com/openipc/decoder/Jieli.java[937-945]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A timeout announcement received after the first keepalive changes the interval but not the pending deadline, so the next keepalive can be too late.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[735-747]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[938-945]
## Recommended Fix
Track the last keepalive send time and recompute the next deadline when an announcement changes the interval. Send immediately if the new deadline has already passed, and test an announcement received after the initial keepalive.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A live toy stream can show as stopped ✓ Resolved
Description
onState(false, ...) clears the transport label and publishes a stopped status without ordering
that update against usable() on the separate UDP thread. When a packet arrives as the CTP thread
declares the stream quiet, the negative status can run after that packet reports connected and
remain visible until another usable packet arrives.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R770-773]

+                        inUse = null;
+                        // Video that stopped says so; the next frame reports "connected" again
+                        status(gen, received.get() ? "disconnected" : "buffering");
+                        first.set(true);
Evidence
The CTP loop bases its quiet transition on a snapshot of lastVideoAt; a separate UDP receiver
invokes the packet callback, whose usable() method can publish connected before the CTP callback
publishes disconnected.

android/app/src/main/java/com/openipc/decoder/Jieli.java[749-765]
android/app/src/main/java/com/openipc/decoder/Jieli.java[859-879]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[764-808]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CTP thread can publish a quiet status after a concurrent UDP packet has restored the connected status.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[749-765]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[764-808]
## Recommended Fix
Serialize state and usable-packet updates, or recheck the latest video timestamp before publishing a negative status, so an older quiet observation cannot override a newer usable packet.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/Jieli.java
Comment thread android/app/src/main/java/com/openipc/decoder/RtspSession.java Outdated
… video (Qodo on #198)

- a KEEP_ALIVE_INTERVAL announced after the first keepalive applies at once: the next
  one is due interval ms after the last one sent, not on the old schedule; test with
  the announcement after the first keepalive
- the quiet status from the CTP thread and "connected" from the UDP thread are
  serialised, and a quiet notice is dropped when video arrived meanwhile
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo findings addressed in 2eacdfa: (1) the keepalive is now due one interval after the last one sent, so a KEEP_ALIVE_INTERVAL announced later applies at once (new test lateIntervalAppliesAtOnce); (2) the quiet status from the CTP thread and "connected" from the UDP thread now take the same lock, and a quiet notice is dropped if video arrived in the meantime. CI green (run 38789039092).

@compot-dev
compot-dev marked this pull request as ready for review October 15, 2026 19:18
@compot-dev
compot-dev merged commit 950277a into feat/flowufo-187 Oct 15, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/jieli-187 branch October 15, 2026 19:19
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add video-only Jieli HF toy drone slots over CTP

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add video-only jieli:// slots using CTP control and existing RTP/JPEG and H.264 playback.
• Bind sockets to toy Wi-Fi and expose stream status, diagnostics, and per-slot statistics.
• Add fake-toy tests and document setup and the unverified video path.
Diagram

sequenceDiagram
    participant Slot as Slot UI
    participant Session as RtspSession
    participant Client as Jieli Client
    participant Network as NetInfo
    participant Toy as HF Toy
    participant Decode as Video Decoder
    participant Stats as Stats API
    Slot->>Session: Open jieli slot
    Session->>Client: Start session
    Client->>Network: Bind TCP and UDP
    Client->>Toy: APP_ACCESS over CTP
    Toy-->>Client: VIDEO_PARAM and timeout
    Client->>Toy: DATE_TIME, keepalive, OPEN_RT_STREAM
    Toy-->>Client: Video to UDP 6666
    Client->>Session: RTP or JPEG
    Session->>Decode: Decode video
    Stats->>Session: Read slot statistics
    Session->>Client: Read Jieli statistics
    Slot->>Session: Stop slot
    Session->>Client: Close session
    Client->>Toy: CLOSE_RT_STREAM
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extend the existing UDP RTP listener
  • ➕ Could consolidate RTP socket reception and packet handling across slot types.
  • ➖ Would require the generic listener to accommodate toy-specific network binding, sender filtering, diagnostics, and CTP-coordinated socket startup.

Recommendation: Keep the dedicated Jieli client for its CTP lifecycle and diagnostic UDP sockets, while reusing the existing decoding paths through RtspSession. Consider extracting shared UDP reception only if another transport needs the same behavior. Device testing is still needed to validate the assumed video path.

Files changed (13) +1962 / -27

Enhancement (8) +1305 / -21
Decoder.javaExpose Jieli slot diagnostics +5/-0

Expose Jieli slot diagnostics

• Adds Jieli statistics to single-view and grid-cell debug output. Labels matching slots as Jieli RTP.

android/app/src/main/java/com/openipc/decoder/Decoder.java

FlowUfo.javaPoint HF-network users toward Jieli slots +4/-4

Point HF-network users toward Jieli slots

• Replaces the unsupported-protocol hint on 192.168.80.x networks with a 'jieli://' suggestion. Makes subnet and sentence helpers available to the Jieli client.

android/app/src/main/java/com/openipc/decoder/FlowUfo.java

Jieli.javaImplement Jieli CTP control and UDP video reception +1126/-0

Implement Jieli CTP control and UDP video reception

• Adds URL parsing, CTP framing and session commands, keepalives, stream retries, and shutdown. Receives expected RTP/JPEG, H.264, or whole-JPEG video from the toy, with network-aware sockets, sender filtering, subnet hints, diagnostics, and statistics.

android/app/src/main/java/com/openipc/decoder/Jieli.java

NetInfo.javaBind CTP sockets to the toy network +51/-15

Bind CTP sockets to the toy network

• Adds Android 6+ network binding for TCP sockets. Shares target-network discovery with the existing UDP binding method.

android/app/src/main/java/com/openipc/decoder/NetInfo.java

QuadCell.javaForward grid-cell Jieli statistics +3/-0

Forward grid-cell Jieli statistics

• Exposes the cell session's Jieli statistics for grid debug output.

android/app/src/main/java/com/openipc/decoder/QuadCell.java

RtspClient.javaForward single-slot Jieli statistics +2/-0

Forward single-slot Jieli statistics

• Exposes Jieli session statistics through the client wrapper.

android/app/src/main/java/com/openipc/decoder/RtspClient.java

RtspSession.javaIntegrate Jieli slots with session playback +111/-1

Integrate Jieli slots with session playback

• Routes 'jieli://' URLs into the new client and forwards received video to existing RTP or JPEG callbacks, switching codecs to follow the received payload type. Coordinates status, statistics, shutdown, and watchdog exclusion with the session lifecycle.

android/app/src/main/java/com/openipc/decoder/RtspSession.java

StreamProbes.javaExclude Jieli slots from single-view stream checks +3/-1

Exclude Jieli slots from single-view stream checks

• Skips checks that could compete with the toy slot for its stream or local listening port.

android/app/src/main/java/com/openipc/decoder/StreamProbes.java

Tests (2) +616 / -1
FlowUfoTest.javaVerify the revised HF subnet hint +2/-1

Verify the revised HF subnet hint

• Changes the expected FLOW-UFO hint on an HF network to recommend 'jieli://192.168.80.1'.

android/app/src/test/java/com/openipc/decoder/FlowUfoTest.java

JieliTest.javaTest CTP framing and fake-toy playback +614/-0

Test CTP framing and fake-toy playback

• Covers message framing, JSON, URLs, subnet hints, and keepalive timing. Loopback tests exercise the CTP session, JPEG reconstruction from RTP, shutdown, missing-video diagnostics, retries, and unreachable targets.

android/app/src/test/java/com/openipc/decoder/JieliTest.java

Documentation (3) +41 / -5
debug.mdDocument Jieli debug statistics +1/-1

Document Jieli debug statistics

• Notes that 'jieli://' slots add a Jieli object to single-view and grid-cell debug output.

docs/debug.md

toys.mdDocument HF toy setup and test reporting +38/-4

Document HF toy setup and test reporting

• Adds the HF/Jieli toy family, URL options, expected CTP and video behavior, status messages, diagnostics, and tester instructions. Clearly marks the video path as unverified and credits its research sources.

docs/toys.md

transports.mdList the new Jieli transport +2/-0

List the new Jieli transport

• Adds 'jieli://' to the transport guide with its control and expected video ports, options, and test-build caveat.

docs/transports.md

Comment on lines +1012 to +1013
lastVideoAt = now();
listener.onRtp(b, 0, len);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Bad packets keep the toy marked as live 🐞 Bug ≡ Correctness

video() updates lastVideoAt for any toy-origin packet with RTP version bits and payload type 26
or 96 before the decoder decides whether the packet is usable. If the toy repeatedly sends malformed
RTP, the assemblers reject it while the slot reports receiving and neither its quiet-state logic nor
its stream-reopen logic runs.
Agent Prompt
## Issue description
Malformed RTP refreshes Jieli video liveness even when the decoder rejects it, preventing quiet-state reporting and stream reopen.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[1005-1019]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[784-809]
## Recommended Fix
Have the media listener report whether the downstream callback accepted a packet or JPEG frame. Refresh `lastVideoAt` only for accepted media; retain separate counters for packets that arrived but were unusable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +780 to +784
for (int p : diagnosticPorts) {
if (p == url.rtpPort) continue;
try {
startThread(openUdp(p, a), false, listener);
} catch (IOException e) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Silent toy slots block other udp streams 🐞 Bug ≡ Correctness

Client.run() opens diagnostic listeners on UDP 1234 and 2224 after five seconds without video and
keeps their sockets open until the Jieli session ends. In a grid, another cell configured to listen
on either port cannot bind its unicast socket for as long as the silent toy slot remains active.
Agent Prompt
## Issue description
Diagnostic UDP listeners retain ports used by other grid streams for the entire silent Jieli session.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[774-789]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[846-887]
## Recommended Fix
Give diagnostic listeners a bounded observation period or packet limit, then close their sockets while retaining the collected counts and first-packet details in stats.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +932 to +933
Integer c = topics.get(m.topic);
topics.put(m.topic, c == null ? 1 : c + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Toy messages can exhaust phone memory 🐞 Bug ☼ Reliability

handle() adds every distinct CTP topic to topics without a size limit, although only individual
message lengths are bounded. A peer sending a continuing series of valid messages with different
topic names grows both the retained map and the copy made for each /stats.json request without
bound.
Agent Prompt
## Issue description
A connected CTP peer can cause unlimited growth in the topic statistics map by sending distinct valid topics.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Jieli.java[929-937]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[1075-1079]
## Recommended Fix
Cap the number of distinct topics retained and aggregate further unseen topics into an overflow count, so stats snapshots remain bounded too.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +817 to +818
});
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Toy reconnects wait up to 30 s after a dropout 🐞 Bug ☼ Reliability

Jieli.Client.run() only returns normally when the slot is stopped. A closed CTP connection or CTP
silence ends it with an IOException instead, so jieli() never reaches return true and
networkLoop never sets played, which means the back-off is never reset. If the toy was off for a
few attempts at first (the delay doubles up to the 30 s maximum) and then streamed fine, every later
dropout still waits about 30 s before reconnecting, and each one counts as another failure through
onFailure, even though the comment says the method behaves "like a PLAY that succeeded".
Agent Prompt
## Issue description
A jieli:// session that connected and requested the stream still ends with an IOException when the toy drops the connection. Because of that, RtspSession.networkLoop never sees `played == true` and never resets the retry back-off (up to 30 s).

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[756-827]
- android/app/src/main/java/com/openipc/decoder/Jieli.java[690-838]

## Recommended Fix
Add a flag on Jieli.Client, e.g. `volatile boolean streamRequested`, and set it when OPEN_RT_STREAM is first sent. In RtspSession.jieli(), wrap `c.run(...)` in a try/catch on IOException. If `c.streamRequested` is true, log the error (and call callback.onFailure if you want), then return true so the back-off resets. Otherwise rethrow.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d094871

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants