Skip to content

feat: say when the device's video decoder stands still (#145) - #147

Merged
compot-dev merged 8 commits into
v2from
fix/decoder-stuck
Oct 13, 2026
Merged

compot-dev merged 8 commits into
v2from
fix/decoder-stuck

Conversation

@compot-dev

@compot-dev compot-dev commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #145 (detection; the cause stays open).

When at least two multi-view cells receive picture data (connected, not paused, H.264/H.265 slices arrived since the last sample, no RTP packet lost that second; MJPEG cells do not count) and none decodes a frame for 20 s in a row, the app shows a banner over the grid:

The video decoder of this device does not respond.
Frames arrive, none is decoded. Restart the device.

Device test (AG-216, CI build 2b9185d): normal 3×3 grid for 90 s — decoder_stuck: false throughout, no banner. The stuck state itself (seen once today, fixed only by a reboot) cannot be produced on demand, so the positive path is covered by the unit test and review.

compot-dev and others added 2 commits October 13, 2026 15:57
…e in several cells, none decoded for 20 s (#145)

A banner over the grid asks to restart the device; /stats.json has decoder_stuck and the
debug page shows it in red. Cells losing packets on the network do not count.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Slow cameras can trigger a false alarm 📎 Requirement gap ≡ Correctness
Description
isReceiving() counts any assembled unit during the last sample but never enforces the required
incoming rate of at least 1 fps. Two connected, unpaused cells sending below that rate can still
satisfy the detector across successive samples and eventually show a device-decoder warning even
though the stated detection condition is not met.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R507-509]

+    boolean isReceiving() {
+        return pausedReason == null && "connected".equals(state) && unitsArrived
+                && !networkLoss;
Evidence
The detector uses QuadCell.isReceiving() to count cells, but the added predicate checks only
connection state, pause state, whether any assembled unit arrived, and network loss. It contains no
incoming-rate threshold, so the implementation does not enforce the sustained-rate condition
required for decoder-stuck detection.

Detect an unresponsive device video decoder
android/app/src/main/java/com/openipc/decoder/QuadCell.java[503-510]

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 receiving predicate accepts any assembled unit in a sampling interval but does not require the incoming rate to be at least 1 fps, allowing slow streams to trigger the device-wide decoder-stuck warning.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[507-509]
## Recommended Fix
Include the measured incoming frame rate in `isReceiving()` and require it to be at least 1 fps, while preserving the connected, unpaused, assembled-unit, and no-network-loss checks.

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


2. Users cannot see repeated key-frame loss ✓ Resolved
Description
QuadCell.sample() records packet gaps only in the internal networkLoss flag and does not expose
a key-frame-loss status. When pushed streams repeatedly lose key-frame packets, the cell still shows
ordinary frame statistics and /stats.json exposes only generic drop or damage counters, so users
receive no explicit loss diagnosis.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R331-334]

+        // Any lost packet, not only one inside a fragmented NAL unit (Qodo on #147)
+        int netLoss = jpeg ? jpegAssembler.droppedCount() : nalAssembler.gapCount();
+        networkLoss = netLoss > lastNetLoss;
+        lastNetLoss = netLoss;
Evidence
Compliance ID 4 requires repeated pushed-stream key-frame loss to be visible in both the cell corner
text and /stats.json. The changed code records a packet-gap boolean for internal filtering, while
the branch's cell text and stats continue to expose only ordinary FPS, generic NAL drops, and
salvage counts rather than a key-frame-loss condition.

Expose pushed-stream key-frame loss
android/app/src/main/java/com/openipc/decoder/QuadCell.java[331-334]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[359-361]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1460-1470]

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

## Issue description
Packet-gap tracking was added for decoder-stuck detection, but repeated pushed-stream key-frame loss is still not surfaced as a clear cell message or dedicated `/stats.json` condition.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[331-334]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1450-1472]
## Recommended Fix
Track repeated incomplete key frames separately from generic packet gaps, include a clear `key frames lost` status in the cell text, and expose the corresponding condition in each cell's `/stats.json` entry.

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


3. A stalled decoder never gets reported ✓ Resolved
Description
checkDecoderStuck() increments stuckSeconds, but showDecoderStuck(false) immediately resets it
to zero on every tick before the threshold. Even when several cells receive frames and none decodes
for longer than 20 seconds, the banner, debug-page warning, and decoder_stuck value never become
active.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1064-1065]

+        if (!on) {
+            stuckSeconds = 0;
Evidence
The tick calls the check, which increments the counter and calls the display method with false
until it reaches 20; that method resets the counter on each such call. The debug field reads the
same unreachable threshold.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1027-1031]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1051-1067]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1419-1420]

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 warning's display method clears the consecutive-tick counter every time the threshold has not yet been reached, so detection cannot complete.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1051-1067]
- android/app/src/test/java/com/openipc/decoder/QuadGridTest.java[79-88]
## Recommended Fix
Keep the counter across qualifying ticks below the threshold. Reset it only when the condition stops qualifying or the grid is left, and add a test that runs the full tick-to-warning transition.

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



Remediation recommended

4. Decoder warning timing cannot be changed 📎 Requirement gap ⚙ Maintainability ⭐ New
Description
The stalled-decoder detector hard-codes STUCK_MS to 20 seconds instead of obtaining the interval
from configuration. isStuck() always applies that fixed value, so deployments cannot tune the
detection window to their stream or device characteristics.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1034-1035]

+    static final long STUCK_MS = 20_000;
+    /** Since when the condition holds, or -1 (monotonic ms; Qodo on #147: time, not ticks). */
Evidence
The compliance criterion requires detection after a configurable interval. The added implementation
declares a fixed STUCK_MS value and isStuck() applies that constant directly, with no
configuration lookup or runtime override.

Detect a stalled device decoder
android/app/src/main/java/com/openipc/decoder/Decoder.java[1034-1035]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1049]

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 stalled-decoder detector uses a compile-time 20-second threshold, so deployments cannot configure the interval required by the decoder-stall detection criteria.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1034-1035]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1049]

## Recommended Fix
Read the stall interval from the app's existing configuration or preferences, provide a documented default of 20 seconds, and use that configured value when evaluating whether the detector has been stuck long enough.

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


5. Missing video surfaces prompt a reboot 🐞 Bug ≡ Correctness ⭐ New
Description
isReceiving() counts a connected cell with arriving picture slices even when its rendering surface
has been destroyed. If this persists in two cells for 20 seconds, neither can create a decoder, yet
the grid displays a device-restart warning.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R518-519]

+        return pausedReason == null && problem == null && "connected".equals(state)
+                && unitsArrived && !networkLoss;
Evidence
Surface destruction clears the surface and closes the decoder without stopping the session or
changing the connection state. Incoming slices can therefore keep setting unitsArrived, while
decoder creation returns early for the missing surface; the detector counts two such cells and shows
its banner after 20 seconds.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[213-225]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[145-153]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[339-345]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[516-520]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[326-338]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1060-1074]

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 cell can keep receiving slices after its surface is destroyed. Two such cells can trigger the device-decoder warning even though decoder creation is blocked by missing surfaces.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[516-520]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[213-225]

## Recommended Fix
Require a non-null, valid surface in `isReceiving()` before counting a cell. Do not require an already-created decoder, since a genuinely stuck decoder may never start.

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


6. Long key-frame waits prompt a reboot ✓ Resolved
Description
nalSink increments assembled for non-key picture slices even though decode() discards those
slices while a cell is in key-frame-only mode or waiting for a key frame. If two connected cameras
deliver only non-key slices for 20 seconds without packet loss, both count as receiving, neither
produces output, and the detector displays the device-restart warning.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R147-149]

+            if (!jpeg && nal.length() > 4 && NalAssembler.isSlice(nal.data()[4], codecH265)) {
+                assembled++;
+            }
Evidence
The new counter includes all picture slices, but the existing decode gate skips non-key units before
they reach the decoder. The new receiving check and timer can therefore classify intentional
filtering as a decoder failure.

android/app/src/main/java/com/openipc/decoder/NalAssembler.java[241-245]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[74-75]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[145-149]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[637-649]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[336-342]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1042-1073]

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 stuck detector counts non-key slices that the grid deliberately skips, so two cameras awaiting key frames can trigger a false reboot warning.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[145-149]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[637-649]
## Recommended Fix
Make the receiving signal account for the current key-frame gate. While key-frame-only mode or waiting for a key frame, do not treat skipped non-key slices as input eligible for the stuck-decoder diagnosis.

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


View review recommended (13)
7. Decoder errors prompt a reboot warning ✓ Resolved
Description
isReceiving() ignores problem, so a cell with a reported decoder error or decoder-limit failure
remains eligible whenever picture slices arrive. If two such cells keep receiving clean streams
without producing output, the detector replaces their specific diagnosis with a device-wide restart
warning after 20 seconds.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R513-515]

+    boolean isReceiving() {
+        return pausedReason == null && "connected".equals(state) && unitsArrived
+                && !networkLoss;
Evidence
Decoder callbacks set a specific problem that the cell status displays, but the newly added
eligibility check does not inspect it. Assembly continues before queueing, allowing the global timer
to advance on those cells.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[130-149]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[347-350]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[513-515]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1042-1073]

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

## Issue description
Cells already reporting decoder errors or decoder-limit failures can contribute to a device-wide stuck diagnosis that recommends a reboot.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[513-515]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1059-1073]
## Recommended Fix
Exclude cells with a known decoder problem from the receiving count, while preserving their existing per-cell error message.

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


8. Software video hides a stuck decoder ✓ Resolved
Description
checkDecoderStuck() treats positive outFps() from any cell as proof that the device’s video
decoder is working, including frames rendered by the software MJPEG path. When two H.264 or H.265
cells receive frames but their hardware decoders produce nothing, a playing software-MJPEG cell
resets the timer on every tick.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1065]

+            if (c.outFps() > 0) anyOutput = true;
Evidence
outFps includes jpegFramesOut, which increases after MJPEG rendering; that rendering can use
BitmapFactory and a canvas instead of the device video decoder. Nevertheless, any positive rate
clears stuckSince.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[319-328]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[673-687]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[52-91]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1042-1049]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1059-1070]

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

## Issue description
Software-rendered frames can prevent detection of a stalled device video decoder.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1059-1069]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[319-328]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[673-687]
## Recommended Fix
Expose whether a cell's recent output came from a relevant device video decoder. Use that signal for `anyOutput`, while continuing to count output from lossy hardware-video cells.

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


9. JPEG losses can trigger a false reboot ✓ Resolved
Description
QuadCell.sample() reads jpegAssembler.droppedCount() on the UI thread, but the receive thread
updates its underlying dropped field without a visibility guarantee. If the read misses a recent
drop, a JPEG cell with frame loss can pass isReceiving(); two such cells with no output can
advance the reboot-warning timer.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R334-335]

+        int netLoss = jpeg ? jpegAssembler.droppedCount() : nalAssembler.gapCount();
+        networkLoss = netLoss > lastNetLoss;
Evidence
sample() uses the JPEG drop count to set networkLoss, which isReceiving() requires to be
false. Unlike the new volatile NAL gap counter, JpegAssembler.dropped is a plain int read and
written by different threads.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[333-339]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[503-510]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[82-105]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[217-223]
android/app/src/main/java/com/openipc/decoder/NalAssembler.java[44-48]

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 detector's UI-thread JPEG loss read has no visibility guarantee for receive-thread writes.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[86-105]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[217-223]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[333-339]
## Recommended Fix
Make the JPEG drop counter safely visible across threads, as the NAL gap counter is, and retain the per-sample loss comparison.

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


10. Metadata-only streams prompt a reboot ✓ Resolved
Description
unitsArrived advances for every NAL emitted by the assembler, including parameter sets that
contain no picture, and isReceiving() interprets that as incoming video frames. If two connected
streams keep sending SPS/PPS or VPS metadata without usable picture slices, they can satisfy the
receiving threshold for 20 seconds despite delivering no frame for a working decoder to display.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R336-338]

+        int units = assembled;
+        unitsArrived = units != lastAssembled;
+        lastAssembled = units;
Evidence
The assembler emits aggregation members individually, and the new counter increments for each
emission without inspecting its NAL type. Two cells need only pass isReceiving() with no decoded
output for the detector to show the warning.

android/app/src/main/java/com/openipc/decoder/NalAssembler.java[113-134]
android/app/src/main/java/com/openipc/decoder/NalAssembler.java[205-222]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[144-151]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[333-339]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[503-510]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1059-1070]

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

## Issue description
Metadata NALs alone can satisfy the detector's incoming-frame condition.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[144-151]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[333-339]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[503-510]
## Recommended Fix
Track received picture-bearing NALs separately from parameter sets and other metadata, and base the receiving condition on picture-bearing input. Keep the existing network-loss exclusion.

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


11. Lost JPEG starts can prompt a reboot ✓ Resolved
Description
sample() uses JpegAssembler.droppedCount() to exclude lossy JPEG cells, but the assembler does
not increment that count when a frame's first fragment is missing. If later marker-bearing fragments
keep arriving in two cells, both can count as receiving without producing frames, and the detector
can attribute the loss to the device decoder.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R332-333]

+        int netLoss = jpeg ? jpegAssembler.droppedCount() : nalAssembler.gapCount();
+        networkLoss = netLoss > lastNetLoss;
Evidence
The assembler ignores nonzero-offset fragments when no frame start was seen, while its drop counter
increases only through drop(). The new receiving check uses that counter, and the packet path
counts marker-bearing packets as incoming frames before JPEG assembly.

android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[165-178]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[219-230]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[331-334]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[590-595]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[490-496]

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 missing first JPEG fragment is not counted as network loss, allowing a lossy cell to contribute to a decoder-stuck warning.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[165-178]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[331-334]
## Recommended Fix
Record a missing frame start when a later fragment, particularly its marker packet, arrives without an active frame. Include that loss in the per-sample receiving check, and test the missing-first-fragment case.

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


12. Unsupported streams can prompt a reboot ✓ Resolved
Description
isReceiving() relies on inFps, which counts RTP marker bits before NalAssembler validates or
emits a NAL unit. If two connected streams repeatedly send marker-bearing packets in an unsupported
NAL format, they can advance the stuck timer despite delivering no decodable input and recording no
packet gap.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R494-495]

+    boolean isReceiving() {
+        return pausedReason == null && "connected".equals(state) && inFps >= 1 && !networkLoss;
Evidence
The packet callback increments framesIn on a marker before calling the assembler. The assembler
can return without emitting an unsupported NAL, and that path need not increment the new gap
counter; isReceiving() nevertheless accepts the resulting input rate.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[590-596]
android/app/src/main/java/com/openipc/decoder/NalAssembler.java[108-139]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[317-334]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[490-496]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1049-1060]

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

## Issue description
Marker-bearing packets can satisfy the decoder-stuck detector even when the NAL assembler rejects or ignores their payloads.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[490-496]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[590-595]
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[85-139]
## Recommended Fix
Track usable assembled video input separately from marker-bit frame counts, and use that signal when deciding whether a cell contributes to decoder-stuck detection. Test streams whose packets carry markers but never emit usable NAL units.

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


13. One-frame-per-second cameras hide a stall ✓ Resolved
Description
isReceiving() requires a sampled inFps of at least 1, but sample() divides the frame count by
the actual interval between stats callbacks. When a camera sends one frame per second and callbacks
run slightly more than one second apart, its measured rate falls below 1 and repeatedly resets the
detector, even if two such cameras keep receiving frames without decoding.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[427]

+        return pausedReason == null && "connected".equals(state) && inFps >= 1 && !networkLoss;
Evidence
sample() uses elapsed time as the FPS denominator, while isReceiving() imposes an exact
threshold and the detector requires two eligible cells on successive checks.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[272-289]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[422-428]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1054-1064]

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 one-frame-per-second stream can measure below 1 FPS because the sampling interval is slightly longer than one second, preventing the stalled-decoder detector from counting it.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[272-289]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[422-428]
## Recommended Fix
Base receiving eligibility on frame arrivals over the sampled interval, with timing tolerance appropriate for one-frame-per-second streams, rather than requiring the computed floating-point rate to reach exactly 1.

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


14. Delayed grid ticks postpone the warning ✓ Resolved
Description
stuckStep() adds one to stuckSeconds per invocation rather than measuring elapsed time. Because
the next stats callback is posted one second after the current callback finishes, UI work or
scheduling delays make twenty counted ticks take longer than twenty seconds even while receiving
cells produce no output.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1046-1048]

+    static int stuckStep(int seconds, int receiving, boolean anyOutput) {
+        return receiving >= 2 && !anyOutput ? seconds + 1 : 0;
+    }
Evidence
The callback is scheduled only after its work completes, whereas the new threshold advances by
exactly one on every invocation regardless of the time between invocations.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1004-1032]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1037-1064]

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 detector counts stats callbacks as seconds, so delayed callbacks postpone the warning beyond the intended duration.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1027-1032]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1037-1071]
## Recommended Fix
Track the monotonic time at which consecutive eligible, output-free observations began and compare elapsed time against the threshold. Clear that start time when the condition breaks or the grid resets.

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


15. Packet losses can escape the clean check ✓ Resolved
Description
NalAssembler.gaps is incremented on the packet-processing thread and read through gapCount() on
the UI thread without a visibility guarantee. When a sampling read observes a stale count,
sample() leaves networkLoss false for that interval, allowing a lossy cell to contribute to a
device-wide stuck diagnosis.
Code

android/app/src/main/java/com/openipc/decoder/NalAssembler.java[47]

+    private int gaps;
Evidence
The packet callback reaches the assembler from the session's packet loop; the stats runnable reads
the plain integer through sample() on the UI thread. No synchronization or volatile declaration
publishes the counter between those paths.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[622-635]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[485-504]
android/app/src/main/java/com/openipc/decoder/NalAssembler.java[44-67]
android/app/src/main/java/com/openipc/decoder/NalAssembler.java[90-93]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1004-1027]

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 new packet-gap counter is shared between packet processing and UI sampling without a visibility guarantee, so a sample can miss a loss.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[44-67]
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[90-93]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[283-289]
## Recommended Fix
Give gap-count writes and reads a thread-safe publication mechanism, such as an atomic counter, and retain the per-sample comparison in the cell.

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


16. The decoder lockup remains unexplained ✓ Resolved
Description
The new decoder-stuck handling records that the issue was seen on one device and that rebooting
helped, but it documents no reproduction path or likely cause. Because the lockup cannot be
reproduced from the added evidence, investigators still cannot determine what lifecycle disruption
leaves the decoder service stuck.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1047-1049]

+    /**
+     * Seen on the AG-216: every cell receives but no decoder gives a frame, and only a reboot
+     * of the device helps (#145). Said on screen instead of black cells and "0.0 fps".
Evidence
The compliance rule requires a documented reproduction path and likely cause for the decoder-service
lockup. The changed comment only states that the condition was observed on the AG-216 and that
rebooting helped, while providing neither a reproduction sequence nor a suspected cause.

Investigate decoder-service lockup
android/app/src/main/java/com/openipc/decoder/Decoder.java[1047-1049]

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 decoder-stuck change notes an observed lockup and reboot recovery but does not document how to reproduce the failure or what likely causes it, leaving the decoder-service investigation incomplete.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1047-1049]
## Recommended Fix
Document the lifecycle sequence, app and camera state, stream configuration, timing, and device conditions that reproduce the lockup, along with the suspected decoder-service failure and evidence supporting that hypothesis. Add the investigation notes to the appropriate project documentation or issue rather than relying only on an inline observation.

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


17. Healthy decoding still prompts a reboot ✓ Resolved
Description
checkDecoderStuck() counts decoded output only from cells that pass isReceiving(), which
excludes a cell with network loss even when that cell decodes frames. Once the timer can reach its
threshold, two clean receiving cells with no output and one lossy cell with continuing output can
produce a device-wide stuck warning despite the working decoder.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1055-1057]

+            if (c == null || !c.isReceiving()) continue;
+            receiving++;
+            if (c.outFps() > 0) decoding++;
Evidence
Output FPS and network loss are sampled independently, but the new loop skips a lossy cell before
checking its output. The existing planner, by contrast, treats output from any playing cell as
evidence of decoding.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1051-1060]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[270-289]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[417-426]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1141-1155]

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 loss filter applies to the output count as well as the receiving count, hiding proof that a decoder is working.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1051-1060]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[417-426]
## Recommended Fix
Count qualifying lossless receivers separately, but cancel the stuck condition when any active cell has decoded output, including a cell with network loss.

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


18. Some packet loss passes the clean check ✓ Resolved
Description
sample() treats an unchanged NalAssembler.lossCount() as evidence that no network loss occurred,
but that counter does not increase for a missing single-packet NAL when no fragmented unit is in
progress. If other marker-bearing frames keep inFps above one, the new receiving check can
classify a lossy cell as clean and include it in a device-wide stuck diagnosis.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R286-288]

+        int netLoss = nalAssembler.lossCount();
+        networkLoss = netLoss > lastNetLoss;
+        lastNetLoss = netLoss;
Evidence
The assembler recognizes a sequence gap but increments lostDropped there only while inFragment
is true. The new predicate relies on that count, while incoming FPS counts marker packets
independently of assembly.

android/app/src/main/java/com/openipc/decoder/NalAssembler.java[86-105]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[283-289]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[425-426]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[482-496]

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 loss counter measures dropped fragmented NAL units, not every missing RTP packet, so it cannot establish that a cell had no network loss.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[283-289]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[425-426]
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[86-105]
## Recommended Fix
Expose a sequence-gap count that includes gaps between complete NAL packets, and use its per-sample change to exclude lossy cells from the receiving count.

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


19. A resumed grid keeps an obsolete warning ✓ Resolved
Description
startStreams() calls showDecoderStuck(false) only in its single-view branch, while pausing a
multi-view grid stops its cells without hiding the root-level banner or clearing its state. Once a
warning has been displayed, pausing and resuming that grid leaves it visible over newly created
cells and preserves decoder_stuck until a subsequent stats tick.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1949]

+            showDecoderStuck(false);
Evidence
Pause removes the stats callback and stops quad streams without resetting the new state. The quad
resume branch recreates cells, whereas only the single-view branch calls the reset; the banner is
attached to the root rather than to the rebuilt grid.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1063-1081]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1811-1822]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1848-1863]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1930-1950]

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 newly added reset covers single-view startup but not pause and recreation of a multi-view grid.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1811-1822]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1848-1863]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1930-1950]
## Recommended Fix
Hide the banner and reset the detector when quad streams stop or before new quad cells are created, so a resumed grid starts with fresh detection state.

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


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 16/18, lines 159/200; both must reach the floor). Router rationale: Bug-dense detector spans timing, stream classification, UI, and debug-state paths.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1047 to +1049
/**
* Seen on the AG-216: every cell receives but no decoder gives a frame, and only a reboot
* of the device helps (#145). Said on screen instead of black cells and "0.0 fps".

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. The decoder lockup remains unexplained 📎 Requirement gap ☼ Reliability

The new decoder-stuck handling records that the issue was seen on one device and that rebooting
helped, but it documents no reproduction path or likely cause. Because the lockup cannot be
reproduced from the added evidence, investigators still cannot determine what lifecycle disruption
leaves the decoder service stuck.
Agent Prompt
## Issue description
The decoder-stuck change notes an observed lockup and reboot recovery but does not document how to reproduce the failure or what likely causes it, leaving the decoder-service investigation incomplete.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1047-1049]

## Recommended Fix
Document the lifecycle sequence, app and camera state, stream configuration, timing, and device conditions that reproduce the lockup, along with the suspected decoder-service failure and evidence supporting that hypothesis. Add the investigation notes to the appropriate project documentation or issue rather than relying only on an inline observation.

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

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
…put of any cell counts, any RTP gap marks a cell lossy, leaving the grid resets the detector

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo → 2e1df42:

  1. A stalled decoder never gets reported — real, thanks: showDecoderStuck(false) reset the count every second. The display no longer touches the count; resetDecoderStuck() does, only when the grid is left. Unit test decoderStuckIsReportedAfterTwentyQuietSeconds walks the 20 ticks.
  2. Healthy decoding still prompts a reboot — output of any cell, a lossy one too, now cancels the count.
  3. Some packet loss passes the clean check — NalAssembler.gapCount() counts every RTP sequence gap; a cell with any gap in the last second is not a clean receiver.
  4. A resumed grid keeps an obsolete warning — stopStreams() resets the detector and hides the banner.
  5. The decoder lockup remains unexplained — that is the open half of AG-216: hardware decoders stop working until reboot — all cells receive, none decodes #145 (cause and reproduction); this PR is only the detection, the issue stays open for the investigation.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo → 2e1df42:

  1. A stalled decoder never gets reported — real, thanks: showDecoderStuck(false) reset the count every second. The display no longer touches the count; resetDecoderStuck() does, only when the grid is left. Unit test decoderStuckIsReportedAfterTwentyQuietSeconds walks the 20 ticks.
  2. Healthy decoding still prompts a reboot — output of any cell, a lossy one too, now cancels the count.
  3. Some packet loss passes the clean check — NalAssembler.gapCount() counts every RTP sequence gap; a cell with any gap in the last second is not a clean receiver.
  4. A resumed grid keeps an obsolete warning — stopStreams() resets the detector and hides the banner.
  5. The decoder lockup remains unexplained — that is the open half of AG-216: hardware decoders stop working until reboot — all cells receive, none decodes #145 (cause and reproduction); this PR is only the detection, the issue stays open for the investigation.

Your assessment is correct:

  • finding 1 is addressed: the counter now persists across qualifying ticks and is reset only by resetDecoderStuck().
  • finding 3 is addressed: output from any cell, including a lossy cell, prevents the stuck diagnosis.
  • finding 4 is addressed: RTP sequence gaps are tracked independently through gapCount().
  • finding 5 is addressed: stopping streams resets and hides the detector state.
  • finding 2 remains open as an investigation/documentation gap. The PR implements detection, but does not document a reproducible lifecycle sequence or suspected decoder-service cause.

* network may decode nothing for that reason alone (#113, #133).
*/
boolean isReceiving() {
return pausedReason == null && "connected".equals(state) && inFps >= 1 && !networkLoss;

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. One-frame-per-second cameras hide a stall 🐞 Bug ≡ Correctness

isReceiving() requires a sampled inFps of at least 1, but sample() divides the frame count by
the actual interval between stats callbacks. When a camera sends one frame per second and callbacks
run slightly more than one second apart, its measured rate falls below 1 and repeatedly resets the
detector, even if two such cameras keep receiving frames without decoding.
Agent Prompt
## Issue description
A one-frame-per-second stream can measure below 1 FPS because the sampling interval is slightly longer than one second, preventing the stalled-decoder detector from counting it.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[272-289]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[422-428]
## Recommended Fix
Base receiving eligibility on frame arrivals over the sampled interval, with timing tolerance appropriate for one-frame-per-second streams, rather than requiring the computed floating-point rate to reach exactly 1.

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

Comment on lines +1046 to +1048
static int stuckStep(int seconds, int receiving, boolean anyOutput) {
return receiving >= 2 && !anyOutput ? seconds + 1 : 0;
}

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. Delayed grid ticks postpone the warning 🐞 Bug ≡ Correctness

stuckStep() adds one to stuckSeconds per invocation rather than measuring elapsed time. Because
the next stats callback is posted one second after the current callback finishes, UI work or
scheduling delays make twenty counted ticks take longer than twenty seconds even while receiving
cells produce no output.
Agent Prompt
## Issue description
The detector counts stats callbacks as seconds, so delayed callbacks postpone the warning beyond the intended duration.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1027-1032]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1037-1071]
## Recommended Fix
Track the monotonic time at which consecutive eligible, output-free observations began and compare elapsed time against the threshold. Clear that start time when the condition breaks or the grid resets.

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

private int dropped;
/** Of those, dropped because a packet of them was lost (not for size) (#113). */
private int lostDropped;
private int gaps;

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. Packet losses can escape the clean check 🐞 Bug ☼ Reliability

NalAssembler.gaps is incremented on the packet-processing thread and read through gapCount() on
the UI thread without a visibility guarantee. When a sampling read observes a stale count,
sample() leaves networkLoss false for that interval, allowing a lossy cell to contribute to a
device-wide stuck diagnosis.
Agent Prompt
## Issue description
The new packet-gap counter is shared between packet processing and UI sampling without a visibility guarantee, so a sample can miss a loss.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[44-67]
- android/app/src/main/java/com/openipc/decoder/NalAssembler.java[90-93]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[283-289]
## Recommended Fix
Give gap-count writes and reads a thread-safe publication mechanism, such as an atomic counter, and retain the per-sample comparison in the cell.

ⓘ 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 2e1df42

…come from its JPEG assembler

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0ea5a2e

…assembler, the threshold is time not ticks, the gap count is published to the UI thread

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo → b009f85:

  • 3, 4, 5 (lost JPEG starts / unsupported streams / 1 fps cameras): a cell counts as receiving when whole units came out of its assembler since the last sample (NAL units or complete JPEG frames, queue drops included) — not marker bits, not a float rate ≥ 1.
  • 6 (delayed ticks): the threshold is 20 s of monotonic time since the condition began (stuckSince / isStuck); the unit test ticks every 1.3 s.
  • 7 (gap count visibility): NalAssembler.gaps is volatile (one writer, the receive thread).
  • 1 (users cannot see repeated key-frame loss): covered by fix: U cell / udp:// slots — lossy pushed stream decoded instead of staying black (#133) #146, merged — a cell decoding damaged key frames says · damaged and /stats.json has nal_dropped / nal_damaged. Not part of this PR.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo → b009f85:

  • 3, 4, 5 (lost JPEG starts / unsupported streams / 1 fps cameras): a cell counts as receiving when whole units came out of its assembler since the last sample (NAL units or complete JPEG frames, queue drops included) — not marker bits, not a float rate ≥ 1.
  • 6 (delayed ticks): the threshold is 20 s of monotonic time since the condition began (stuckSince / isStuck); the unit test ticks every 1.3 s.
  • 7 (gap count visibility): NalAssembler.gaps is volatile (one writer, the receive thread).
  • 1 (users cannot see repeated key-frame loss): covered by fix: U cell / udp:// slots — lossy pushed stream decoded instead of staying black (#133) #146, merged — a cell decoding damaged key frames says · damaged and /stats.json has nal_dropped / nal_damaged. Not part of this PR.

The changes address findings 4, 5, 6, and 7:

  • Finding 4: receiving is based on assembled units, avoiding marker-only unsupported streams.
  • Finding 5: unitsArrived avoids relying on an exact inFps >= 1 threshold.
  • Finding 6: stuckSince and monotonic elapsed time correctly handle delayed ticks.
  • Finding 7: gaps is now volatile, providing cross-thread visibility.

Finding 3 is not fully addressed by this diff. I don’t see a JpegAssembler change that records a missing first fragment, so droppedCount() can still remain unchanged for that case. Finding 1 can remain out of scope if the required · damaged and /stats.json support was delivered separately in the merged change you referenced.

Comment on lines +507 to +509
boolean isReceiving() {
return pausedReason == null && "connected".equals(state) && unitsArrived
&& !networkLoss;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Slow cameras can trigger a false alarm 📎 Requirement gap ≡ Correctness

isReceiving() counts any assembled unit during the last sample but never enforces the required
incoming rate of at least 1 fps. Two connected, unpaused cells sending below that rate can still
satisfy the detector across successive samples and eventually show a device-decoder warning even
though the stated detection condition is not met.
Agent Prompt
## Issue description
The receiving predicate accepts any assembled unit in a sampling interval but does not require the incoming rate to be at least 1 fps, allowing slow streams to trigger the device-wide decoder-stuck warning.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[507-509]

## Recommended Fix
Include the measured incoming frame rate in `isReceiving()` and require it to be at least 1 fps, while preserving the connected, unpaused, assembled-unit, and no-network-loss checks.

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

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

…unt for the stuck-decoder check; JPEG drop count published to the UI thread

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo → 6e41104:

  • 4 Software video hides a stuck decoder / 5 JPEG losses: MJPEG cells are left out of the check entirely (usesVideoDecoder()) — they may be drawn without the device's video decoder; the JPEG drop counter is volatile anyway.
  • 6 Metadata-only streams: only picture slices (NalAssembler.isSlice) count as arriving units, not parameter sets.
  • 1 Slow cameras / ≥ 1 fps: declined — the PR text said "≥ 1 fps", but the previous round (finding 5) showed a float rate threshold misses 1 fps cameras; the condition is now "slices arrived since the last sample", for 20 s in a row, in at least two cells. PR description updated.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo → 6e41104:

  • 4 Software video hides a stuck decoder / 5 JPEG losses: MJPEG cells are left out of the check entirely (usesVideoDecoder()) — they may be drawn without the device's video decoder; the JPEG drop counter is volatile anyway.
  • 6 Metadata-only streams: only picture slices (NalAssembler.isSlice) count as arriving units, not parameter sets.
  • 1 Slow cameras / ≥ 1 fps: declined — the PR text said "≥ 1 fps", but the previous round (finding 5) showed a float rate threshold misses 1 fps cameras; the condition is now "slices arrived since the last sample", for 20 s in a row, in at least two cells. PR description updated.

Your rationale appears to address the current open findings:

  • finding 4 — Software video hides a stuck decoder: checkDecoderStuck() now skips MJPEG cells via usesVideoDecoder(), so software-rendered output cannot mask a hardware decoder stall.
  • finding 5 — JPEG losses: JpegAssembler.dropped is now volatile, providing the required cross-thread visibility.
  • finding 6 — Metadata-only streams: assembled increments only for picture slices, so parameter-set-only NALs do not satisfy the receiving condition.
  • finding 1 — Slow cameras: the implementation intentionally uses slice arrivals during each sample rather than a floating-point FPS threshold, avoiding the one-frame-per-second timing problem.

Based on the supplied diff, findings 4–6 look addressed, and finding 1 is a requirements/interpretation dispute rather than an unambiguously missing implementation. The published review has not yet reflected these conclusions.

Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6e41104

…ll with its own decoder problem is left out of the stuck-decoder check

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo → 72f5be6: Long key-frame waits — on key frames only (or waiting for one), only key slices count as arriving; Decoder errors — a cell with its own problem (decoder error, decoder limit) is left out of the check and keeps its message.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo → 72f5be6: Long key-frame waits — on key frames only (or waiting for one), only key slices count as arriving; Decoder errors — a cell with its own problem (decoder error, decoder limit) is left out of the check and keeps its message.

Confirmed in 72f5be6: the fixes address finding 4 (long key-frame waits) and finding 5 (decoder errors). Both are marked resolved in the current review.

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 72f5be6

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev
compot-dev merged commit af88fb7 into v2 Oct 13, 2026
4 checks passed
@compot-dev
compot-dev deleted the fix/decoder-stuck branch October 13, 2026 17:39
Comment on lines +1034 to +1035
static final long STUCK_MS = 20_000;
/** Since when the condition holds, or -1 (monotonic ms; Qodo on #147: time, not ticks). */

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. Decoder warning timing cannot be changed 📎 Requirement gap ⚙ Maintainability

The stalled-decoder detector hard-codes STUCK_MS to 20 seconds instead of obtaining the interval
from configuration. isStuck() always applies that fixed value, so deployments cannot tune the
detection window to their stream or device characteristics.
Agent Prompt
## Issue description
The stalled-decoder detector uses a compile-time 20-second threshold, so deployments cannot configure the interval required by the decoder-stall detection criteria.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1034-1035]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1049]

## Recommended Fix
Read the stall interval from the app's existing configuration or preferences, provide a documented default of 20 seconds, and use that configured value when evaluating whether the detector has been stuck long enough.

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

Comment on lines +518 to +519
return pausedReason == null && problem == null && "connected".equals(state)
&& unitsArrived && !networkLoss;

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

5. Missing video surfaces prompt a reboot 🐞 Bug ≡ Correctness

isReceiving() counts a connected cell with arriving picture slices even when its rendering surface
has been destroyed. If this persists in two cells for 20 seconds, neither can create a decoder, yet
the grid displays a device-restart warning.
Agent Prompt
## Issue description
A connected cell can keep receiving slices after its surface is destroyed. Two such cells can trigger the device-decoder warning even though decoder creation is blocked by missing surfaces.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[516-520]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[213-225]

## Recommended Fix
Require a non-null, valid surface in `isReceiving()` before counting a cell. Do not require an already-created decoder, since a genuinely stuck decoder may never start.

ⓘ 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 8fd5206

compot-dev added a commit that referenced this pull request Oct 15, 2026
…put of any cell counts, any RTP gap marks a cell lossy, leaving the grid resets the detector

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…assembler, the threshold is time not ticks, the gap count is published to the UI thread

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…unt for the stuck-decoder check; JPEG drop count published to the UI thread

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…ll with its own decoder problem is left out of the stuck-decoder check

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
feat: say when the device's video decoder stands still (#145)
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.

1 participant