Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ Versions after 2.17 are `MAJOR.MINOR.PATCH` (2.17 is followed by 2.17.1 or 2.18.
`docs/testing.md`, "Versions and releases"); the headings below keep the numbers as they
were released.

## 2.18.3 — 2026-10-21

- `udp://` slots and the U cell: a thread of its own reads the socket into a ring of 1024
datagrams, so key-frame bursts no longer overflow the small socket buffer an app gets
(`rmem_max`) while the decoder or the UI hold up the receive; the log shows the buffer the
kernel gave. While the U cell receives a push, the other slots get no stream checks, which
made the box drop datagrams. `/stats.json` adds `udp` to the cell: `rcvbuf`, `received`,
`ring_dropped`, `rtp_lost` and, where the app may read it, `socket_drops` (#251).

## 2.18.2 — 2026-10-20

- Single view: the decoder is set up for the stream's size from its SPS, so streams above
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
applicationId "com.openipc.decoder"
minSdk 21
targetSdk 36
versionCode 45
versionName "2.18.2"
versionCode 46
versionName "2.18.3"

buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
}
Expand Down
28 changes: 24 additions & 4 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,8 @@ public void run() {
v.setVisibility(showStats ? View.VISIBLE : View.GONE);
if (log) Log.i(TAG, "Quad-" + i + ": " + line);
}
// Not only on debug requests: a push that starts ends the checks within a second
streamProbes.hold(pushReceived());
checkDecoderStuck(cells);
planDecoding(cells, now);
updateDebugUrls();
Expand Down Expand Up @@ -1697,6 +1699,9 @@ private Map<String, Object> debugState() {
if (jl != null) c.put("jieli", jl);
Map<String, Object> hb = cell.heartbeatStats();
if (hb != null) c.put("heartbeat", hb);
// A udp:// push: a full socket buffer or ring, or loss on the way (#251)
Map<String, Object> udp = cell.udpListenStats();
if (udp != null) c.put("udp", udp);
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
if (cell.rotate() != 0) c.put("rotate", cell.rotate());
if (cell.mirror()) {
c.put("mirror", true);
Expand All @@ -1723,8 +1728,20 @@ private void updateProbes() {
urls[i] = mHosts[i] != null && !mHosts[i].isEmpty() && !mHosts[i].equals(DEFAULT_URL)
? mHosts[i] : null;
}
boolean push = pushReceived();
streamProbes.hold(push);
streamProbes.update(urls, mTransports, udpViewShown ? null : mHost,
mResumed && (udpViewShown || !quadEnabled));
mResumed && (udpViewShown ? !push : !quadEnabled));
}

/**
* The U cell receives a push: no checks of other cameras then, their RTSP sessions made
* the box drop datagrams of the push (#251).
*/
private boolean pushReceived() {
QuadCell[] cells = quadCells;
QuadCell u = udpViewShown && cells != null ? cells[U_CELL] : null;
return u != null && u.isConnected();
}

/**
Expand Down Expand Up @@ -1754,7 +1771,9 @@ private String probeText(int i, long nowMs) {
if (skip != null) return skip;
StreamProbe.Result r = probeResult(i);
if (r != null) return r.text(nowMs) + (streamProbes.isRunning(i) ? " \u00b7 checking" : "");
return streamProbes.isRunning(i) ? "Checking stream\u2026" : "Waiting for a stream check";
if (streamProbes.isRunning(i)) return "Checking stream\u2026";
return pushReceived() ? "Not checked while the U cell receives a push"
: "Waiting for a stream check";
Comment on lines +1775 to +1776

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

8. Pushes leave probe state marked waiting 🐞 Bug ≡ Correctness

probeText() now says a slot is not checked during a U-cell push, but probeSlots() still assigns
that slot's probe.state to waiting when it has no previous result. During a push, consumers of
/stats.json receive contradictory text and state for the same slot, despite probing being held.
Agent Prompt
## Issue description
During a U-cell push, a slot without a prior result reports that checks are paused in its text but remains `waiting` in its JSON probe state.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1771-1776]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1839-1847]

## Recommended Fix
Use the active-push condition when assigning the JSON probe state so an unprobed slot is `not checked` while checks are held. Keep the text and state consistent.

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

}

/**
Expand Down Expand Up @@ -1818,8 +1837,9 @@ private List<Object> probeSlots() {
StreamProbe.Result r = probeResult(i);
Map<String, Object> p = new LinkedHashMap<>();
p.put("state", r != null ? (r.ok ? "ok" : "error")
: streamProbes.isRunning(i) ? "checking" : u || StreamProbes.skipReason(
mHosts[i], DEFAULT_URL, udpViewShown ? null : mHost) != null
: streamProbes.isRunning(i) ? "checking" : u || pushReceived()
|| StreamProbes.skipReason(mHosts[i], DEFAULT_URL,
udpViewShown ? null : mHost) != null
? "not checked" : "waiting");
if (r != null) {
p.put("checked_s_ago", (now - r.atMs) / 1000);
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/QuadCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ static boolean isKeyFrameNal(int type, boolean h265) {

/** The cooingdv heartbeat of an RTSP cell once it used one (#187), else null. */
java.util.Map<String, Object> heartbeatStats() { return session.heartbeatStats(); }
java.util.Map<String, Object> udpListenStats() { return session.udpListenStats(); }

/** Degrees counter-clockwise the frames are turned (#200, #201), 0 for none. */
int rotate() { return session.rotate(); }
Expand Down Expand Up @@ -516,6 +517,9 @@ private void checkUdpStall() {
/** On key frames only, a frame comes once per GOP, which can be several seconds (#85). */
static final long KEY_FRAMES_LIVE_MS = 10_000;

/** A stream is coming in: "connected", paused or not. */
boolean isConnected() { return "connected".equals(state); }

/**
* Connected, not paused and a frame shown within the last {@code ms} milliseconds (#98);
* on key frames within KEY_FRAMES_LIVE_MS, or the cell would blink between key frames.
Expand Down
Loading
Loading