Repository navigation
feat: say when the device's video decoder stands still (#145) #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f8820c
2b9185d
2e1df42
0ea5a2e
b009f85
6e41104
72f5be6
8fd5206
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import android.os.SystemClock; | ||
| import android.provider.MediaStore; | ||
| import android.util.Log; | ||
| import android.util.TypedValue; | ||
| import android.view.GestureDetector; | ||
| import android.view.Gravity; | ||
| import android.view.KeyEvent; | ||
|
|
@@ -478,6 +479,7 @@ private boolean leaveUdpView() { | |
| menuManager.setUdpShown(false); | ||
| stopQuadCells(); | ||
| quadContainer.setVisibility(View.GONE); | ||
| resetDecoderStuck(); | ||
| mSurface.setVisibility(View.VISIBLE); | ||
| return true; | ||
| } | ||
|
|
@@ -731,6 +733,7 @@ private void stopQuad() { | |
| } | ||
|
|
||
| quadContainer.setVisibility(View.GONE); | ||
| resetDecoderStuck(); | ||
| mSurface.setVisibility(View.VISIBLE); | ||
| } | ||
|
|
||
|
|
@@ -1017,13 +1020,91 @@ public void run() { | |
| v.setVisibility(showStats ? View.VISIBLE : View.GONE); | ||
| if (log) Log.i(TAG, "Quad-" + i + ": " + line); | ||
| } | ||
| checkDecoderStuck(cells); | ||
| planDecoding(cells, now); | ||
| updateDebugUrls(); | ||
| refreshUdpHint(); | ||
| quadContainer.postDelayed(this, 1000); | ||
| } | ||
| }; | ||
|
|
||
| // ====================== The device's decoder stands still (#145) ====================== | ||
|
|
||
| /** Time with frames arriving in several cells and none decoded: the device is stuck. */ | ||
| static final long STUCK_MS = 20_000; | ||
| /** Since when the condition holds, or -1 (monotonic ms; Qodo on #147: time, not ticks). */ | ||
| private long stuckSinceMs = -1; | ||
| private TextView stuckBanner; | ||
|
|
||
| /** | ||
| * One observation of the detector: since when at least two cells have received whole | ||
| * frames and no cell at all decoded one (#145), or -1 when that does not hold now. | ||
| */ | ||
| static long stuckSince(long sinceMs, long nowMs, int receiving, boolean anyOutput) { | ||
| if (receiving < 2 || anyOutput) return -1; | ||
| return sinceMs >= 0 ? sinceMs : nowMs; | ||
| } | ||
|
|
||
| static boolean isStuck(long sinceMs, long nowMs) { | ||
| return sinceMs >= 0 && nowMs - sinceMs >= STUCK_MS; | ||
| } | ||
|
|
||
| private boolean decoderStuck() { | ||
| return isStuck(stuckSinceMs, System.nanoTime() / 1_000_000); | ||
| } | ||
|
|
||
| /** | ||
| * 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". | ||
|
Comment on lines
+1056
to
+1058
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. The decoder lockup remains unexplained 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
|
||
| */ | ||
| private void checkDecoderStuck(QuadCell[] cells) { | ||
| int receiving = 0; | ||
| boolean anyOutput = false; | ||
| for (QuadCell c : cells) { | ||
| if (c == null) continue; | ||
| // Output of any cell, a lossy one too, shows the decoder works (Qodo on #147) | ||
| // Only H.264/H.265 cells: MJPEG may be drawn without the video decoder at all | ||
| if (!c.usesVideoDecoder()) continue; | ||
| if (c.outFps() > 0) anyOutput = true; | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
|
||
| if (c.isReceiving()) receiving++; | ||
| } | ||
| long now = System.nanoTime() / 1_000_000; | ||
| stuckSinceMs = stuckSince(stuckSinceMs, now, receiving, anyOutput); | ||
| showDecoderStuck(isStuck(stuckSinceMs, now)); | ||
| } | ||
|
|
||
| /** Leaving or rebuilding the grid: a fresh count, no banner (Qodo on #147). */ | ||
| private void resetDecoderStuck() { | ||
| stuckSinceMs = -1; | ||
| showDecoderStuck(false); | ||
| } | ||
|
|
||
| private void showDecoderStuck(boolean on) { | ||
| if (!on) { | ||
| if (stuckBanner != null) stuckBanner.setVisibility(View.GONE); | ||
| return; | ||
| } | ||
| if (stuckBanner == null) { | ||
| stuckBanner = new TextView(this); | ||
| stuckBanner.setText("The video decoder of this device does not respond.\n" | ||
| + "Frames arrive, none is decoded. Restart the device."); | ||
| stuckBanner.setTextColor(Color.WHITE); | ||
| stuckBanner.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); | ||
| stuckBanner.setGravity(Gravity.CENTER); | ||
| stuckBanner.setBackgroundColor(0xCC000000); | ||
| int pad = (int) (16 * getResources().getDisplayMetrics().density); | ||
| stuckBanner.setPadding(pad, pad, pad, pad); | ||
| ((FrameLayout) findViewById(R.id.decoder)).addView(stuckBanner, new FrameLayout.LayoutParams( | ||
| FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, | ||
| Gravity.CENTER)); | ||
| } | ||
| if (stuckBanner.getVisibility() != View.VISIBLE) { | ||
| Log.w(TAG, "No cell decodes although frames arrive: the device's decoder is stuck"); | ||
| } | ||
| stuckBanner.setVisibility(View.VISIBLE); | ||
| stuckBanner.bringToFront(); | ||
| } | ||
|
|
||
| private void startStatsTick() { | ||
| quadContainer.removeCallbacks(quadStatsTick); | ||
| decodePlanner = newPlanner(true); | ||
|
|
@@ -1363,6 +1444,8 @@ private Map<String, Object> debugState() { | |
| dec.put("learnt", planner.isLearnt()); | ||
| dec.put("failed_load", planner.failedLoad()); | ||
| m.put("decoder", dec); | ||
| // Frames arrive in several cells, none decodes: restart the device (#145) | ||
| m.put("decoder_stuck", decoderStuck()); | ||
| } | ||
| if (udpViewShown) { | ||
| Map<String, Object> udp = new LinkedHashMap<>(); | ||
|
|
@@ -1753,6 +1836,7 @@ public void run() { | |
|
|
||
| private void stopStreams() { | ||
| lastJpegFrameMs = 0; | ||
| resetDecoderStuck(); | ||
| mSurface.removeCallbacks(singleUdpCheck); | ||
| // The single view's stream ends here: it is no longer "connected" for the debug page | ||
| singleState = "disconnected"; | ||
|
|
@@ -1847,6 +1931,7 @@ private void startStreams() { | |
| } else { | ||
| // The views may still be in Quad layout from before the pause | ||
| quadContainer.setVisibility(View.GONE); | ||
| resetDecoderStuck(); | ||
| mSurface.setVisibility(View.VISIBLE); | ||
| boolean configured = mHost != null && !mHost.isEmpty() && !mHost.equals(DEFAULT_URL); | ||
| if (configured) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,6 +142,14 @@ void setAudioSink(AudioManager sink, int route) { | |
| } | ||
| }); | ||
| this.nalSink = nal -> { | ||
| // Picture slices out of the assembler, before the queue: what the video decoder | ||
| // would get (#145); parameter sets alone are no picture (Qodo on #147) | ||
| // On key frames only, just the slices decode() really passes on (Qodo on #147) | ||
| if (!jpeg && nal.length() > 4 && NalAssembler.isSlice(nal.data()[4], codecH265) | ||
| && (!(keyFramesOnly || waitingKeyFrame) || isKeyNal( | ||
| NalAssembler.fragment(nal.data()[4], codecH265), codecH265))) { | ||
| assembled++; | ||
| } | ||
| if (!nalQueue.offer(nal)) { | ||
| // Rate-limited: one aggregation packet can carry many NAL units | ||
| if (queueFullDrops++ % 100 == 0) { | ||
|
|
@@ -328,6 +336,13 @@ String sample(long nowMs) { | |
| lastIn = in; | ||
| lastOut = out; | ||
| lastLost = lost; | ||
| // Any lost packet, not only one inside a fragmented NAL unit (Qodo on #147) | ||
| int netLoss = jpeg ? jpegAssembler.droppedCount() : nalAssembler.gapCount(); | ||
| networkLoss = netLoss > lastNetLoss; | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
|
||
| int units = assembled; | ||
| unitsArrived = units != lastAssembled; | ||
| lastAssembled = units; | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
|
||
| lastNetLoss = netLoss; | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
|
||
| lastSampleMs = nowMs; | ||
| checkUdpStall(); | ||
|
|
||
|
|
@@ -479,6 +494,31 @@ boolean showsVideo(long ms) { | |
| return pausedReason == null && "connected".equals(state) && renderedWithin(window); | ||
| } | ||
|
|
||
| /** NAL units were lost on the network in the last second (#145). */ | ||
| private volatile boolean networkLoss; | ||
| /** | ||
| * NAL units or JPEG frames out of the assembler: unlike marker bits (inFps), only data the | ||
| * decoder could take; queue drops included (Qodo on #147). | ||
| */ | ||
| private volatile int assembled; | ||
| private int lastAssembled; | ||
| /** Some arrived since the last sample, whatever the sampling interval. */ | ||
| private volatile boolean unitsArrived; | ||
| private int lastNetLoss; | ||
|
|
||
| /** | ||
| * Connected, not paused, and whole frames arrive (#145): a cell losing packets on the | ||
| * network may decode nothing for that reason alone (#113, #133). | ||
| */ | ||
| /** H.264/H.265, decoded by the device's video decoder (not MJPEG, #145). */ | ||
| boolean usesVideoDecoder() { return !jpeg; } | ||
|
|
||
| boolean isReceiving() { | ||
| // A cell with its own decoder problem (error, limit) says so itself (Qodo on #147) | ||
| return pausedReason == null && problem == null && "connected".equals(state) | ||
| && unitsArrived && !networkLoss; | ||
|
Comment on lines
+518
to
+519
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. Missing video surfaces prompt a reboot 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
|
||
| } | ||
|
|
||
| private boolean renderedWithin(long ms) { | ||
| if (!jpeg) return decoder.renderedWithin(ms); | ||
| long shown = lastJpegShownMs; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4. Decoder warning timing cannot be changed
📎 Requirement gap⚙ MaintainabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools