Skip to content

feat: MJPEG cells in multi-view (#132) - #139

Merged
compot-dev merged 4 commits into
v2from
feat/mjpeg-multiview
Oct 13, 2026
Merged

compot-dev merged 4 commits into
v2from
feat/mjpeg-multiview

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

MJPEG cameras play in the grid instead of being paused with "MJPEG: single view only".

  • JpegRenderer: the MJPEG path of the single view (hardware video/jpeg decoder where the platform exposes one, else BitmapFactory + lockCanvas) moved into one class, used by the single view and by every cell.
  • A cell assembles RTP/JPEG (RFC 2435) with its own JpegAssembler and shows the frames on its SurfaceView from its decode thread.
  • The cheap mode of the planner (Multi-view: adaptive decoding — full rate or key frames per cell, learnt device capacity #85) for MJPEG is one frame per second ("MJPEG reduced · 1.0 fps"), full rate after a promotion.
  • Codec change MJPEG → H.264/H.265 on the same cell: a surface drawn with lockCanvas() cannot take a decoder, so the SurfaceView is hidden and shown for a new surface; no decoder until it is there.

Part of #132.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Device test on AG-216 (Aceline-1), CI build d3ea50c, 3×3 grid + U:

  • .133 /stream=2 (RTSP RTP/JPEG, 1280×720, 5 fps) in a multi-view cell: picture shown, corner 1280×720 MJPEG reduced · 1.0 fps, no losses.
  • Planner promoted the MJPEG cell to full rate: 5–7 fps out at 5–6 fps in (MJPEG 6/5 fps); later demoted again when the U cell lost frames — normal planner behaviour.
  • Single view MJPEG still works; switching the slot MJPEG → H.264 shows live H.264, no black surface.
  • Slides mode: MJPEG cell stays at 1 frame per second.
  • logcat: 0 FATAL EXCEPTION / ANR in, no fallback to software JPEG decoding.

Note: in the cheap mode the rate reads 0.8–1.0 fps, because the next frame after 1 s arrives up to one frame interval (200 ms) late.

@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 (0) 📘 Rule violations (0) 📎 Requirement gaps (2) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. MJPEG planning uses a guessed budget 📎 Requirement gap ➹ Performance
Description
JpegCodec.find() selects a codec name and MIME type but never reads its declared video throughput
or publishes it to MediaCodecManager.declaredBlocksPerSec. When a device relies on the hardware
JPEG path, the planner therefore keeps the generic default budget instead of the selected decoder's
1920×1080 capacity, affecting promotion and capacity learning for every MJPEG cell.
Code

android/app/src/main/java/com/openipc/decoder/JpegCodec.java[R52-55]

       try {
           for (MediaCodecInfo ci : new MediaCodecList(MediaCodecList.ALL_CODECS)
                   .getCodecInfos()) {
-                if (ci.isEncoder()) continue;
+                if (ci.isEncoder() || softwareOnly(ci)) continue;
Evidence
The changed JPEG codec discovery only filters and selects a matching codec from MediaCodecList; it
does not query decoder capabilities. The planner initializes from
MediaCodecManager.declaredBlocksPerSec or a generic default, while the existing capability
publication is performed only by the video decoder manager, so a hardware JPEG-only multi-view
configuration cannot use its declared throughput.

Learn and react to decoder capacity
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[52-58]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[219-241]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1053]

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 hardware JPEG path selects a decoder without querying its declared video capabilities, so multi-view planning cannot initialize its budget from the selected decoder's throughput as required by the capacity-learning model.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[52-58]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[219-241]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1053]
## Recommended Fix
After selecting and successfully configuring the hardware JPEG codec, query its `VideoCapabilities` for 1920×1080 support and publish the resulting macroblocks-per-second value through the shared decoder-capacity mechanism, while preserving the existing fallback when capability information is unavailable.

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


2. MJPEG cells exceed decoder instance limits 📎 Requirement gap ☼ Reliability
Description
JpegCodec.decode() creates and starts each hardware JPEG decoder without acquiring or releasing a
shared instance-budget slot. When multiple MJPEG cells exceed the device's declared limit, they
independently attempt codec creation and fall back to software instead of reporting the applicable
decoder limit or preserving the last-resort pause behavior.
Code

android/app/src/main/java/com/openipc/decoder/JpegCodec.java[R135-145]

+            synchronized (JpegCodec.class) {
+                dec = chosen != null ? chosen : find();
+                boolean byName = dec == null;
+                if (byName) {
+                    if (byNameFailed) throw new IllegalStateException("no JPEG decoder");
+                    dec = BY_NAME[0];
+                }
+                try {
+                    c = MediaCodec.createByCodecName(dec[0]);
+                } catch (Exception e) {
+                    if (byName) byNameFailed = true;
Evidence
The changed JPEG path directly creates and starts MediaCodec under a class lock but never calls
the repository's shared InstanceBudget methods. The existing video decoder path acquires, learns,
and releases instance slots, proving that the JPEG path bypasses the required codec-limit
accounting.

Respect codec instance limits and reserve pausing as a last resort
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[135-160]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[151-190]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[337-389]

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

## Issue description
Hardware MJPEG decoders bypass the shared instance-budget accounting used by H.264 and H.265. Multiple cells can therefore exceed the device's decoder limit, while failures are converted to per-cell software fallback instead of enforcing the applicable limit and notifying the planner.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[135-160]
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[50-67]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[151-190]
## Recommended Fix
Acquire a shared `InstanceBudget` slot for the selected JPEG MIME before creating the codec, learn the codec's declared maximum instance count after creation, release the slot on every configure, start, surface, and stream failure, and release it when the renderer closes the codec. When acquisition fails, expose the JPEG decoder limit to the cell planner so it reports the applicable limit and reserves pausing for overload after reduced-rate decoding has also failed.

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


3. Software MJPEG codecs bypass fallback ✓ Resolved
Description
JpegCodec.find() accepts every non-encoder that advertises an MJPEG MIME type, while
JpegRenderer.render() treats the result as a hardware decoder candidate. When a device exposes
only a software JPEG codec, the app configures that codec instead of selecting the intended
hardware-only path or bitmap fallback, so decoder capacity and performance assumptions are wrong.
Code

android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[R45-49]

+        if (!hwFailed && !drawn && JpegCodec.candidate() != null) {
+            int[] size = JpegAssembler.size(frame.data(), frame.length());
+            if (size != null) {
+                try {
+                    return codec.decode(frame, s, size[0], size[1], current) ? size : null;
Evidence
The renderer added by this PR invokes the codec candidate as its first path, while the branch's
probe filters only encoders and MIME types, not software decoders. A software codec can therefore be
selected and configured through the path introduced for the renderer.

Probe for an MJPEG hardware decoder
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[42-49]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[39-56]

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 renderer activates `JpegCodec.candidate()`, but the codec probe accepts any non-encoder and can select a software MJPEG decoder as the hardware path.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[45-49]
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[39-56]
## Recommended Fix
Filter `MediaCodecInfo` entries with the platform hardware-acceleration indicator or an equivalent hardware-only check before returning a candidate, while retaining the bitmap fallback when no usable hardware decoder remains.

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


View action required (3)
4. MJPEG overloads do not lower load ✓ Resolved
Description
decodeJpeg() returns when JpegRenderer.render() produces no frame without recording a JPEG loss,
and the planner’s sampled losses include queue drops and H.264/H.265 input drops but no
JPEG-specific loss counter. When JpegCodec cannot obtain an input buffer or a frame produces no
decoded output, repeated MJPEG overload cannot contribute to loss-based demotion or capacity
learning, allowing promotion beyond the measured budget.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R572-579]

+        int[] size = jpegRenderer.render(frame, s,
+                () -> running && jpeg && s == surface);
+        if (size == null) return;
+        lastJpegDecodeMs = now;
+        jpegWidth = size[0];
+        jpegHeight = size[1];
+        lastJpegShownMs = now;
+        jpegFramesOut++;
Evidence
The cell path increments jpegFramesOut only after the renderer produces output, while the JPEG
codec can fail to queue a frame when no input buffer is available or produce no output. Neither case
contributes a JPEG-specific loss to lostFps(), so the planner cannot use those failures for
demotion or capacity learning.

Adapt decoding to measured losses and stability
android/app/src/main/java/com/openipc/decoder/QuadCell.java[565-579]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[143-164]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1148-1153]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[140-169]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[295-303]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1148-1161]

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 MJPEG decode path does not report frames that cannot be queued or produce no decoded output to the planner’s sampled loss rate. Distinguish actual JPEG losses from ordinary delayed output.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[140-169]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[295-303]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[565-579]
## Recommended Fix
Track JPEG frames dropped because no decoder input buffer is available and frames that produce no decoded output, without counting ordinary delayed output as a loss. Include those losses in the sampled loss rate used by `plannerCell()`, and reset or scope the counter consistently with the existing decoder loss counters.

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


5. Packet loss leaves JPEG cells stalled ✓ Resolved
Description
QuadCell.onPacket() now assembles MJPEG frames, but checkUdpStall() still reads loss exclusively
from nalAssembler. When missing UDP fragments cause jpegAssembler to discard frames, the JPEG
loss count never advances the stall counter, so the session is not told to fall back from UDP
despite receiving packets without a picture.
Code

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

+        if (jpeg) return jpegAssembler.assemble(buf, offset, length, nalSink::onNal);
Evidence
The JPEG assembler increments its own dropped counter on fragment mismatch, while the stall check
requires an increase in the NAL assembler's loss count before it can report a broken UDP stream.

android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[211-218]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[243-249]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[394-425]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[536-540]

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 UDP stall detector cannot observe frames discarded by the newly active JPEG assembler.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[391-425]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[536-540]
## Recommended Fix
Use the active assembler's loss counter in the UDP stall check and reset its comparison baseline appropriately on codec changes.

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


6. One failing JPEG decoder freezes every MJPEG cell ✓ Resolved
Description
JpegRenderer.hwFailed is static, so a hardware decoder failure in one renderer makes every grid
cell and the single view skip their own hardware decoding branch. If one cell hits the decoder
instance limit, other renderers with a JpegCodec still connected to their surface reach
lockCanvas(), which throws and returns null on every frame, leaving those cells frozen.
Code

android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[R30-33]

+    /** The hardware decoder failed once in this run: the software path from then on. */
+    private static volatile boolean hwFailed;
+
+    private final JpegCodec codec = new JpegCodec();
Evidence
Each grid cell and the single view constructs its own JpegRenderer, but all instances consult and
set the same static hwFailed flag. render() reaches codec.decode and codec.release() only
inside the !hwFailed branch, so once another instance sets the flag, a renderer whose codec still
holds its surface falls through to s.lockCanvas(null); that call throws because the surface is
connected to a MediaCodec producer, and the RuntimeException handler returns null.
JpegCodec.decode creates one MediaCodec per instance, making decoder instance limits reachable
with several MJPEG cameras.

android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[42-87]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[110-136]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[80-80]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[30-55]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[76-81]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1591-1594]

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

## Issue description
`JpegRenderer.hwFailed` is static, so a hardware JPEG failure in one stream sends unrelated renderers to the software path while their codecs may still hold their surfaces. Their `lockCanvas()` calls then fail on every frame.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[30-58]
## Recommended Fix
Make `hwFailed` an instance field so only the renderer whose decoder failed falls back. Release that renderer's codec before calling `lockCanvas()` wherever the software path is taken. If a device-wide signal for unavailable hardware decoding is needed, keep it separate and set it only when codec creation fails, not for instance-limit or per-stream errors.

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



Remediation recommended

7. A replaced camera can show corrupted frames ✓ Resolved
Description
QuadCell.onSdp() calls jpegAssembler.reset(), but that method leaves cached quantization tables
from the previous JPEG stream intact. If a new source behind the same cell uses the same quality
identifier and a frame omits its tables, the assembler embeds the old source’s tables in that frame.
Code

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

+        jpegAssembler.reset();
Evidence
The PR adds a persistent JPEG assembler to each cell and resets it on SDP changes. Its reset method
clears partial-frame state but not the table maps; the assembly path explicitly reuses a cached
entry when a frame supplies zero-length tables for an existing quality identifier.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[154-157]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[492-516]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[90-98]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[138-171]

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 new MJPEG stream can reuse quantization tables cached from a previous source in the same cell, producing incorrectly decoded frames.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[513-516]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[90-98]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[140-145]
## Recommended Fix
Clear the assembler’s cached quantization tables and precision values when resetting it for a new stream. Add a test that resets after caching tables, then verifies that a zero-table frame cannot reuse the previous stream’s tables.

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


8. MJPEG cells starting together lose hardware decoding ✓ Resolved
Description
JpegCodec.byNameTried and chosen are static and shared by every cell, but decode() locks only
its own instance. On a device whose JPEG decoder is reachable only by name (the AG-216, through
BY_NAME), the first cell sets byNameTried = true before its decoder exists. Until that cell sets
chosen, every other cell sees candidate() return null or decode() throw "no JPEG decoder".
Those cells set hwFailed or draw with lockCanvas (drawn = true), and both keep them on
BitmapFactory for good.
Code

android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[R50-61]

+        if (!hwFailed && !drawn && JpegCodec.candidate() != null) {
+            int[] size = JpegAssembler.size(frame.data(), frame.length());
+            if (size != null) {
+                try {
+                    software = false;
+                    return codec.decode(frame, s, size[0], size[1], current) ? size : null;
+                } catch (Exception e) {
+                    codec.release();
+                    // The surface was replaced meanwhile: not the decoder's fault, try again
+                    if (!current.holds()) return null;
+                    Log.w(TAG, "Hardware MJPEG decoder failed, decoding in software", e);
+                    hwFailed = true;
Evidence
decode() is synchronized on its own instance and sets byNameTried = true before
createByCodecName/configure, while chosen is set only after start() succeeds. candidate()
returns null when byNameTried && chosen == null. Before this PR only the single view used
JpegCodec, so no two instances raced; now every grid cell has its own JpegRenderer and JpegCodec,
each on its own decode thread, and they start together.

android/app/src/main/java/com/openipc/decoder/JpegCodec.java[35-38]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[104-109]
android/app/src/main/java/com/openipc/decoder/JpegCodec.java[124-150]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[613-622]

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

## Issue description
JpegCodec's static `byNameTried`/`chosen` are checked and set under per-instance locks. With several MJPEG cells, one cell's by-name probe that is still in progress makes the other cells give up on the hardware decoder for good (`hwFailed`, or `drawn` after a canvas draw).
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[104-150]
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[50-64]
## Recommended Fix
Guard the decoder choice (`find()`/BY_NAME probe and setting `chosen`/`byNameTried`) with a static lock. Make other instances wait for the probe's result instead of treating 'tried but not yet chosen' as 'no decoder'. For example, use a tri-state static (UNKNOWN/PROBING/OK/FAILED) and have `candidate()` return the BY_NAME entry while the probe is still running. Only set `byNameTried` to failed after the probe really fails.

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


9. A cell can freeze when its camera switches to MJPEG ✓ Resolved
Description
decode() checks isJpeg != jpeg once, then calls decoder.decodeFrame()/createDecoder()
without checking again. onSdp() can switch jpeg to true and call decoder.closeDecoder() in
between, and the H.264 frame the decode thread holds then creates a new video decoder on the
surface. Nothing closes that decoder while the stream is MJPEG: JpegCodec.configure fails and sets
hwFailed, and every later lockCanvas() throws, so the cell shows no picture until its surface is
destroyed.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R561-568]

+        boolean isJpeg = buffer.length() >= 2 && (buffer.data()[0] & 0xFF) == 0xFF
+                && (buffer.data()[1] & 0xFF) == 0xD8;
+        if (isJpeg != jpeg) return;
+        if (jpeg) {
+            decodeJpeg(buffer);
+            return;
+        }
+        if (buffer.length() < 5 || surfacePending) return;
Evidence
onSdp closes the video decoder only once, when it sees MJPEG. decode() reads jpeg once at the
start and later calls createDecoder(codecH265) without checking it again, and MediaCodecManager
creates a decoder whenever decodeFrame reports there is none. While jpeg is true only
decodeJpeg runs, so the decoder stays connected to the surface. JpegRenderer.render then sets
hwFailed when configure throws, and its software branch returns null on every lockCanvas
failure.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[493-506]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[558-588]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[53-93]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[326-330]

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

## Issue description
QuadCell.decode() checks the frame's path against `jpeg` once. If onSdp() switches to MJPEG and closes the decoder after that check, the decode thread can create a new H.264/H.265 decoder on the surface. MJPEG then cannot use the surface (both hardware and lockCanvas fail), and the cell stays frozen.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[558-588]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[493-506]
## Recommended Fix
Check `jpeg` again right before `decoder.createDecoder(...)` and after `decodeFrame`. If it is now true, call `decoder.closeDecoder()` and return. A sturdier fix keeps a generation counter that onSdp increments; the decode thread compares it under MediaCodecManager's lock, or closes the decoder after creation if the generation changed. As a safety net, have `decodeJpeg` call `decoder.closeDecoder()` when `decoder.hasDecoder()` is true.

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


View review recommended (4)
10. Software JPEG consumes hardware budget ✓ Resolved
Description
QuadCell supplies JPEG resolution and rate to the planner, but isSoftwareDecoder() still queries
only the video decoder that onSdp() closes for MJPEG. On devices using BitmapFactory, the
planner therefore treats software-rendered JPEG as hardware load, which can keep video cells at
reduced rate despite available hardware capacity.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R351-357]

boolean knowsLoad() {
-        return decoder.outWidth() > 0 && inAvg > 0;
+        return width() > 0 && inAvg > 0;
}
/** Macroblocks per second at full rate (#85). */
long fullLoad() {
-        return DecodePlanner.fullLoad(decoder.outWidth(), decoder.outHeight(), inAvg);
+        return DecodePlanner.fullLoad(width(), height(), inAvg);
Evidence
The renderer's fallback calls BitmapFactory, whereas the planner's pinned/software classification
comes solely from the separate video decoder; non-pinned cells contribute load to the hardware
budget.

android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[59-86]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[339-357]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[481-486]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1148-1161]
android/app/src/main/java/com/openipc/decoder/DecodePlanner.java[150-161]

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 planner charges software JPEG rendering against the video hardware decoder budget.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[42-86]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[340-357]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1148-1161]
## Recommended Fix
Expose whether each JPEG renderer is using bitmap decoding and classify that cell as software-rendered when constructing planner cells, without hiding genuine JPEG queue losses.

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


11. Reconnected JPEG cells retain old frames ✓ Resolved
Description
onSdp() drains completed frames only when the codec changes and does not reset jpegShowing or
the previous JPEG dimensions. On a same-codec reconnect, queued frames from the former connection
can be shown under the new SDP, while the planner can continue treating the cell as playing with its
previous picture size before the new stream renders.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R475-479]

+        boolean wasJpeg = jpeg;
+        if (sdp.videoJpeg != wasJpeg || sdp.videoH265 != codecH265) {
+            // Frames of the previous codec must not reach the new path (Qodo on #131)
+            Frame f;
+            while ((f = nalQueue.poll()) != null) cellFramePool.recycle(f);
Evidence
RTSP invokes onSdp() on a new connection, but the queue-drain condition excludes same-codec
reconnects. JPEG playback state is reset only by stop(), while decoding() and picture size
continue to use those retained fields.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[435-451]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[237-241]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[440-457]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[475-505]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[565-584]

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 same-codec reconnection retains queued JPEG frames and playback state from the previous connection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[475-505]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[440-457]
## Recommended Fix
Recycle queued frames on every new SDP and invalidate the prior JPEG showing, timestamp and dimensions until a frame from the new connection has been rendered.

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


12. Codec switches can misroute a cell frame ✓ Resolved
Description
onSdp() removes frames still in nalQueue, but decode() selects the JPEG or video path using
the mutable jpeg value rather than the codec that produced its frame. If the decode worker has
already polled a frame when an SDP changes between JPEG and H.264/H.265, that old frame can enter
the new codec's path despite the queue drain.
Code

android/app/src/main/java/com/openipc/decoder/QuadCell.java[R545-548]

+        if (jpeg) {
+            decodeJpeg(buffer);
+            return;
+        }
Evidence
The decode executor removes a frame before calling decode(), so the SDP callback's queue drain
cannot remove that frame. The newly added JPEG branch reads the current volatile codec flag, which
the callback changes independently.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[200-216]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[475-505]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[539-559]

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

## Issue description
An in-flight cell frame can be decoded through the path selected by a newer SDP.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[200-216]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[475-505]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[539-559]
## Recommended Fix
Associate queued frames with the connection or codec generation that assembled them, and discard an in-flight frame if that generation no longer matches before selecting a decode path.

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


13. Grid cell can stay blank after MJPEG→H.264 ✓ Resolved
Description
onSdp sets surfacePending = true and relies on setVisibility(GONE) followed by a nested
post(VISIBLE) to make the SurfaceView destroy and recreate its surface; only surfaceCreated
clears the flag. Both runnables usually run before the next traversal, so SurfaceView never sees the
GONE state and gets no new surface. The flag also survives stop()/start(), and start() picks
up an existing surface without clearing it, so decode() drops every H.264/H.265 NAL unit for good.
Code

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

+                surfacePending = true;
+                SurfaceView v = view;
+                if (v != null) v.post(() -> {
+                    v.setVisibility(View.GONE);
+                    v.post(() -> v.setVisibility(View.VISIBLE));
+                });
Evidence
Only surfaceCreated resets surfacePending. start() takes view.getHolder().getSurface()
directly and never clears the flag, and stop() does not reset it either. decode() returns early
whenever surfacePending is true. The single view does not have this problem because it builds a
new Surface synchronously with recreateVideoSurface() and clears its flag right after.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[164-186]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[232-251]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[544-549]
android/app/src/main/java/com/openipc/decoder/Decoder.java[205-213]

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

## Issue description
After MJPEG→H.264, the cell waits for `surfaceCreated` to clear `surfacePending`. GONE and VISIBLE are set in back-to-back posts, which can be coalesced before layout, so the surface may never be recreated. `stop()`/`start()` also never clear the flag.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[488-496]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[160-176]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[232-251]
## Recommended Fix
Set VISIBLE in a callback that runs after a traversal, e.g. `postOnAnimation` twice or a ViewTreeObserver OnPreDraw/OnGlobalLayout listener, or in `surfaceDestroyed` after GONE, so the GONE state really reaches SurfaceView. Reset `surfacePending` in `stop()`. In `start()`, when the cell had drawn with a canvas, recreate the surface rather than reusing a stale one.

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



Informational

14. Stopping a cell mid-packet can crash the receiver ✓ Resolved
Description
QuadCell.stop() calls jpegAssembler.reset() without coordinating with an in-flight assemble(),
clearing its header and the tablesByQ and precisionByQ HashMaps while the network thread may
still read them. Because RtspSession.stop() does not wait for the receive callback, a reset
between fragment assembly and marker completion can leave the header null, while a clear between
tablesByQ.containsKey(q) and unboxing precisionByQ.get(q) can throw a NullPointerException;
concurrent clears and puts can also corrupt a map.
Code

android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[R100-101]

+        tablesByQ.clear();
+        precisionByQ.clear();
Evidence
RtspSession.stop() is documented to stop without waiting and only interrupts its threads; the UDP
packet lambda checks the generation once before calling onPacket, so a callback can remain in
flight during teardown. The new QuadCell.stop() reset clears assembler state that assemble()
reads at JPEG marker completion, and assemble() checks tablesByQ.containsKey(q), gets the table,
then unboxes precisionByQ.get(q) without a lock.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[174-187]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[529-546]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[150-154]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[238-246]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[172-186]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[689-697]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[242-246]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[95-102]
android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[185-204]

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

## Issue description
`QuadCell.stop()` can reset its JPEG assembler while an RTP callback is assembling a packet. The reset can clear the header before marker completion or clear the HashMaps between related reads, causing a receive-callback failure, a NullPointerException, or map corruption.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[242-246]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[513-516]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[550-554]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[95-102]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[148-162]
- android/app/src/main/java/com/openipc/decoder/JpegAssembler.java[175-204]
## Recommended Fix
Remove the UI-thread assembler reset from `stop()`; the `onSdp()` reset on the session thread already covers a new stream. If the stop-time reset must remain, serialize every `reset()` and `assemble()` call on the same per-assembler lock, including stream-change resets, and keep packet completion and its state reads inside that critical section. Store tables and precision together in one value object so assembly obtains both from a single lookup.

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


15. Every MJPEG packet allocates a throwaway sink ✓ Resolved
Description
onPacket() passes nalSink::onNal to jpegAssembler.assemble(), and a bound method reference
creates a new object on every call. This runs for every video RTP packet of every MJPEG cell, on the
receive path that the code elsewhere keeps free of garbage (the SurfaceView comment cites about 1
MB/s of garbage with four streams).
Code

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

+        if (jpeg) return jpegAssembler.assemble(buf, offset, length, nalSink::onNal);
Evidence
nalSink is a NalAssembler.Sink field. Converting it to a JpegAssembler.Sink with a method
reference allocates a new wrapper on each call, unlike the H.264 path, which passes the existing
nalSink instance.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[143-151]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[164-167]

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

## Issue description
`jpegAssembler.assemble(buf, offset, length, nalSink::onNal)` allocates a new method-reference object for every MJPEG RTP packet.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[553]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[143-157]
## Recommended Fix
Add a `private final JpegAssembler.Sink jpegSink;` field and set it once in the constructor (`jpegSink = nalSink::onNal;` after nalSink is set). Then pass `jpegSink` in `onPacket()`.

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


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 thread android/app/src/main/java/com/openipc/decoder/JpegRenderer.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
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
Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
Comment thread android/app/src/main/java/com/openipc/decoder/JpegRenderer.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/QuadCell.java Outdated
…against the path, surface recreated after a layout pass, JPEG losses and UDP stalls counted, software JPEG off the hardware budget, software codecs skipped

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

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo findings 1–9 fixed in 9758984. Re-tested on AG-216 (build 9758984): MJPEG cell in the grid at full rate MJPEG 5/5 fps; Majestic restarted on .133 → Connection refused → reconnects to MJPEG 5/5 fps (same-codec reconnect, #6); single view MJPEG and MJPEG → H.264 in the slot fine; 0 FATAL EXCEPTION / ANR in. The canvas paths (#8, #9) are not reachable on AG-216 (hardware JPEG decoder), so they are covered by review only.

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

Copy link
Copy Markdown

Qodo findings 1–9 fixed in 9758984. Re-tested on AG-216 (build 9758984): MJPEG cell in the grid at full rate MJPEG 5/5 fps; Majestic restarted on .133 → Connection refused → reconnects to MJPEG 5/5 fps (same-codec reconnect, #6); single view MJPEG and MJPEG → H.264 in the slot fine; 0 FATAL EXCEPTION / ANR in. The canvas paths (#8, #9) are not reachable on AG-216 (hardware JPEG decoder), so they are covered by review only.

Thanks for the detailed validation. The retest covers the reported reconnect, MJPEG rendering, codec transition, and stability scenarios. The published review still shows finding 2 as open; the other findings are marked resolved. No review status change was made here.

Comment thread android/app/src/main/java/com/openipc/decoder/QuadCell.java
Comment thread android/app/src/main/java/com/openipc/decoder/JpegRenderer.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 9758984

…at a time picks the JPEG decoder, a stale video decoder is closed before MJPEG is drawn

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

Copy link
Copy Markdown
Collaborator Author

Qodo round 2 → a3dab50:

  • A replaced camera can show corrupted frames: JpegAssembler.reset() clears the cached tables; unit test aNewStreamDoesNotReuseTheTablesOfTheOldOne.
  • MJPEG cells starting together lose hardware decoding: the decoder choice runs under a class lock; candidate() keeps offering the by-name decoder while a probe is in progress and only a failed creation marks it unusable.
  • A cell can freeze when its camera switches to MJPEG: the decode thread closes a video decoder left on the surface before it draws a JPEG frame.
  • MJPEG overloads do not lower load: frames without an input buffer are counted as losses since 9758984 (JpegCodec.inputDropped → sample()). A queued frame without output yet is delayed output, not a loss; a decoder that stays silent for 15 frames is dropped for the software path. Leaving it at that.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo round 2 → a3dab50:

  • A replaced camera can show corrupted frames: JpegAssembler.reset() clears the cached tables; unit test aNewStreamDoesNotReuseTheTablesOfTheOldOne.
  • MJPEG cells starting together lose hardware decoding: the decoder choice runs under a class lock; candidate() keeps offering the by-name decoder while a probe is in progress and only a failed creation marks it unusable.
  • A cell can freeze when its camera switches to MJPEG: the decode thread closes a video decoder left on the surface before it draws a JPEG frame.
  • MJPEG overloads do not lower load: frames without an input buffer are counted as losses since 9758984 (JpegCodec.inputDropped → sample()). A queued frame without output yet is delayed output, not a loss; a decoder that stays silent for 15 frames is dropped for the software path. Leaving it at that.

Thanks for the round-two update. Based on your summary, the changes address findings 5, 6, and 7. Finding 2 remains an intentional design decision: queued frames are treated as delayed output, while input-buffer failures and prolonged decoder silence are handled separately. The published review still lists finding 2 as open until a new review updates its status.

Comment on lines +135 to +145
synchronized (JpegCodec.class) {
dec = chosen != null ? chosen : find();
boolean byName = dec == null;
if (byName) {
if (byNameFailed) throw new IllegalStateException("no JPEG decoder");
dec = BY_NAME[0];
}
try {
c = MediaCodec.createByCodecName(dec[0]);
} catch (Exception e) {
if (byName) byNameFailed = 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.

Action required

1. Mjpeg cells exceed decoder instance limits 📎 Requirement gap ☼ Reliability

JpegCodec.decode() creates and starts each hardware JPEG decoder without acquiring or releasing a
shared instance-budget slot. When multiple MJPEG cells exceed the device's declared limit, they
independently attempt codec creation and fall back to software instead of reporting the applicable
decoder limit or preserving the last-resort pause behavior.
Agent Prompt
## Issue description
Hardware MJPEG decoders bypass the shared instance-budget accounting used by H.264 and H.265. Multiple cells can therefore exceed the device's decoder limit, while failures are converted to per-cell software fallback instead of enforcing the applicable limit and notifying the planner.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[135-160]
- android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[50-67]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[151-190]

## Recommended Fix
Acquire a shared `InstanceBudget` slot for the selected JPEG MIME before creating the codec, learn the codec's declared maximum instance count after creation, release the slot on every configure, start, surface, and stream failure, and release it when the renderer closes the codec. When acquisition fails, expose the JPEG decoder limit to the cell planner so it reports the applicable limit and reserves pausing for overload after reduced-rate decoding has also failed.

ⓘ 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/JpegAssembler.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 a3dab50

…t may be in it, one JPEG sink per cell

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

Copy link
Copy Markdown
Collaborator Author

/review

Comment on lines 52 to +55
try {
for (MediaCodecInfo ci : new MediaCodecList(MediaCodecList.ALL_CODECS)
.getCodecInfos()) {
if (ci.isEncoder()) continue;
if (ci.isEncoder() || softwareOnly(ci)) continue;

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. Mjpeg planning uses a guessed budget 📎 Requirement gap ➹ Performance

JpegCodec.find() selects a codec name and MIME type but never reads its declared video throughput
or publishes it to MediaCodecManager.declaredBlocksPerSec. When a device relies on the hardware
JPEG path, the planner therefore keeps the generic default budget instead of the selected decoder's
1920×1080 capacity, affecting promotion and capacity learning for every MJPEG cell.
Agent Prompt
## Issue description
The hardware JPEG path selects a decoder without querying its declared video capabilities, so multi-view planning cannot initialize its budget from the selected decoder's throughput as required by the capacity-learning model.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/JpegCodec.java[52-58]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[219-241]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1048-1053]

## Recommended Fix
After selecting and successfully configuring the hardware JPEG codec, query its `VideoCapabilities` for 1920×1080 support and publish the resulting macroblocks-per-second value through the shared decoder-capacity mechanism, while preserving the existing fallback when capability information is unavailable.

ⓘ 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 1c3fb86

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo round 4 (1c3fb86): findings 13–15 resolved. The two remaining requirement gaps are left on purpose:

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

Copy link
Copy Markdown

Qodo round 4 (1c3fb86): findings 13–15 resolved. The two remaining requirement gaps are left on purpose:

Understood. Your rationale makes both remaining items intentional design choices rather than accidental omissions:

  • finding 1 — MJPEG planning uses a guessed budget: acceptable if JPEG work is deliberately charged against the shared, loss-learned video budget and a separate JPEG capacity model is out of scope. The tradeoff is that the budget is not based on the hardware JPEG decoder’s declared throughput.
  • finding 2 — MJPEG cells exceed decoder instance limits: acceptable if per-cell BitmapFactory fallback is the required behavior. The implementation should still ensure failed hardware codec attempts release all resources and that software-rendered JPEG load is excluded from the hardware decoder budget.

The published review still shows both as open; this comment explains why you are retaining them, but does not close them.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Correction to my device reports above: on the AG-216 the hardware JPEG decoder is never used — as found in #135, Android 10 does not list video/jpeg and creating OMX.allwinner.video.decoder.mjpeg by name fails (error 0xfffffffe), so every MJPEG test here ran on the software path (BitmapFactory + lockCanvas). I missed the fallback line in logcat and wrote "no fallback to software" and "canvas paths not reachable on AG-216" — both wrong.

What that means for the tests: the software/canvas path was exercised on the device — MJPEG cell in the grid (reduced 1 fps and full 5–7 fps, corner · software), same-codec reconnect after a Majestic restart (build 1c3fb86, MJPEG 5/5 fps again, 0 FATAL/ANR), and MJPEG → H.264 in the single view of the slot (live H.264, no black surface). Not exercised on hardware: the hardware JPEG decoder path and the in-cell MJPEG → H.264 switch on the same cell (no lab camera changes its codec between MJPEG and H.264 live).

@compot-dev
compot-dev merged commit ea5c093 into v2 Oct 13, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/mjpeg-multiview branch October 13, 2026 16:14
compot-dev added a commit that referenced this pull request Oct 13, 2026
Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 13, 2026
compot-dev added a commit that referenced this pull request Oct 13, 2026
…come from its JPEG assembler

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…against the path, surface recreated after a layout pass, JPEG losses and UDP stalls counted, software JPEG off the hardware budget, software codecs skipped

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…at a time picks the JPEG decoder, a stale video decoder is closed before MJPEG is drawn

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…t may be in it, one JPEG sink per cell

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
compot-dev added a commit that referenced this pull request Oct 15, 2026
Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
compot-dev added a commit that referenced this pull request Oct 15, 2026
…come from its JPEG assembler

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev pushed a commit that referenced this pull request Oct 15, 2026
The TextureView's frames go through the app's own GL composition and window
buffer; on a Mali-G31 TV box (Android 10) that buffer's queueBuffer stalled
for seconds, growing with every grid/single switch (up to 14 s in 60 cycles).
With a SurfaceView, as the grid cells use, the stalls are gone (#76).

- Zoom and pan: the view is laid out zoom times the fitted size and moved by
  the pan; its surface keeps the fitted size (setFixedSize). Same gestures,
  clamping and double-tap reset as before, on every Android version.
- Long-press screenshot and the debug page snapshot: PixelCopy on the
  snapshot thread (Snapshots.copy), the UI thread does not wait. Before
  Android 7 a screenshot says it is not available, the snapshot cell says
  "no picture".
- MJPEG to H.264/H.265: the view is hidden and shown again for a new surface,
  as a grid cell does (#139); the decoder waits until surfaceCreated() and is
  closed in surfaceDestroyed().
- Version 2.09.
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