Skip to content

feat: per-slot Mirror in the camera settings (#226) - #246

Merged
compot-dev merged 6 commits into
v2from
feat/mirror-226
Oct 20, 2026
Merged

compot-dev merged 6 commits into
v2from
feat/mirror-226

Conversation

@compot-dev

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

Copy link
Copy Markdown
Collaborator

Closes #226.

Mirror in the camera settings (a checkbox under Turn): flips the picture left to right, after the turn, in the single view and the grid, for H.264, H.265 and MJPEG.

Why a separate drawing path. The hardware decoder draws straight onto a SurfaceView; setScaleX(-1) on it is ignored (the system composes the surface) and MediaFormat.KEY_ROTATION has no mirror (#201, #225).

Change.

  • GlMirror: for a mirrored H.264/H.265 slot the decoder renders into a SurfaceTexture; an OpenGL ES 2 renderer on its own thread draws each frame onto the view's surface with one transform (turn counter-clockwise, then flip on the screen). It also does the slot's turn, so KEY_ROTATION is not set on this path. Unmirrored slots keep the decoder drawing straight onto the SurfaceView (ANR on AG-216: RenderThread blocked in eglSwapBuffers with 8 multi-view SurfaceViews #76).

  • MediaCodecManager: the GL path belongs to the decoder and is released after it on every release path (close, pause, codec error, lost race), so the view's surface is free for the next decoder. If EGL fails, the picture is shown unmirrored (logged).

  • MJPEG: flipped while drawn in software; the hardware JPEG decoder cannot mirror, so a mirrored MJPEG slot is decoded in software.

  • The single view's layout asks the decoder whether it swaps the sides (swapsSides()), which also covers the GL path's turn on Android 5.

  • Settings: mirror_<slot> per slot; /stats.json: mirror: true in single and in cells. README (three languages), docs/usage.md, docs/debug.md.

  • Local Docker build as CI: testDebugUnitTest (incl. GlMirrorTest: the 8 orientations), lintDebug, assembleDebug

  • Aceline-1 (CI build 30402e8): single and grid, H.264 / H.265 / MJPEG, Mirror × Turn, snapshots and screenshots, fps in the 3×3 grid

Mirror flips the picture left to right, after the slot's Turn, in the
single view and the grid. A SurfaceView cannot be mirrored with
setScaleX(-1) and KEY_ROTATION has no mirror, so a mirrored H.264/H.265
slot renders through GlMirror: the decoder outputs into a SurfaceTexture
and an OpenGL ES 2 renderer on its own thread draws each frame turned
and flipped onto the view's surface. Unmirrored slots keep the decoder
drawing straight onto the SurfaceView (#76). The GL path is released
after the decoder, so the view's surface is free for the next one; if
EGL fails, the picture is shown unmirrored. MJPEG is flipped while it
is drawn in software (the hardware JPEG decoder cannot mirror).

The single view's layout now asks the decoder whether it swaps the
picture's sides, which covers the GL path's turn on any Android version.
/stats.json gets mirror: true for mirrored slots and cells.
#226)

Saving the settings restarts the single view. The old decode thread was
still busy with a frame, created a decoder with the old turn and mirror,
and won the race against the new thread; the new one's attempt failed on
the busy surface and still left its turn in appliedRotation, so the
picture had the new layout but the old orientation. Now the old thread
stops before creating a decoder, closeDecoder() discards a creation that
is still running, and appliedRotation is set only by the decoder that
stays.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Device test, Aceline-1 (Android 10, Allwinner H616), CI build 30402e8 (2.18.0):

Check Result
Single view, H.265 2560×1920, Turn 90° + Mirror picture upright and flipped (OSD text reads mirrored), 20 fps
Grid 3×3: slot 1 H.264 Mirror, slot 6 H.265 Turn 90° + Mirror both drawn correctly; slot 1 at its full 18/18 fps, the other cells as before (U cell 30–36 fps)
MJPEG (HTTP, 1280×720) with Mirror flipped, 5 fps (software path)
/snapshot.jpg mirrored cells come out mirrored
Screenshot (long press) 1920×2560, turned and mirrored, as on the screen
/stats.json mirror: true in single and in the cells; gone after Mirror is turned off
Mirror off / Turn Auto again picture back to normal without a restart
Logcat 0 FATAL EXCEPTION, 0 ANR in; ui_thread: ok

Not from this PR: in /snapshot.jpg a turned (90°) slot in the single view is stretched to the 16:9 cell, with or without Mirror; on the screen it is correct. Same on the Turn path from 2.17.

@compot-dev
compot-dev marked this pull request as ready for review October 20, 2026 08:08
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add per-slot camera mirroring for single and grid views

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add per-slot Mirror settings for H.264, H.265, and MJPEG in single and grid views.
• Route mirrored video through OpenGL and mirrored MJPEG through software drawing; preserve direct
 rendering otherwise.
• Prevent stale decoder creation after restart and document the setting and debug output.
Diagram

graph TD
  Settings["Slot settings"] --> Session["RTSP session"] --> Type{"Stream type"} -->|H.26x| Codec["Video decoder"] --> Mirror{"Mirror enabled"} -->|Yes| GL["GL mirror"] --> View["View surface"]
  Type -->|MJPEG| JPEG["JPEG renderer"] --> View
  Mirror -->|No| View
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Route every H.26x slot through GL
  • ➕ Unifies video rotation and mirroring in one rendering path.
  • ➖ Adds a GL thread, surface, and frame draw to every slot, including unmirrored grid cells.
  • ➖ Replaces an established direct-rendering path unnecessarily.

Recommendation: Keep the conditional GL path: it enables mirroring without adding rendering overhead to unmirrored slots. Review decoder-to-EGL release ordering and the fallback path particularly closely, since both affect whether a surface can be reused.

Files changed (16) +499 / -29

Enhancement (8) +437 / -21
Decoder.javaConnect per-slot Mirror settings to playback +20/-8

Connect per-slot Mirror settings to playback

• Persists Mirror for each slot and passes it to single-view and grid sessions and renderers. Reports mirrored streams in debug state, uses decoder-applied rotation for layout, and prevents an old decode thread from creating a decoder after restart.

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

GlMirror.javaRender turned and mirrored video through OpenGL ES +319/-0

Render turned and mirrored video through OpenGL ES

• Adds a dedicated GL thread that receives decoder frames through a SurfaceTexture and draws them onto the view surface with Turn followed by Mirror. Provides synchronous setup and teardown so the surface can be reused.

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

JpegRenderer.javaMirror MJPEG frames in software +8/-4

Mirror MJPEG frames in software

• Bypasses hardware JPEG decoding when Mirror is enabled and flips frames on the Canvas after applying Turn.

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

MediaCodecManager.javaSelect and own the mirrored video path +68/-7

Select and own the mirrored video path

• Routes mirrored H.264/H.265 decoder output through GlMirror while retaining direct output otherwise, with a logged fallback if GL setup fails. Releases GL after the codec on decoder shutdown paths and prevents losing or cancelled creations from publishing their rotation state.

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

MenuManager.javaAdd a Mirror checkbox to camera settings +8/-1

Add a Mirror checkbox to camera settings

• Shows a per-slot Mirror checkbox beneath Turn and saves its value with the other camera settings.

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

QuadCell.javaApply Mirror to grid cells +5/-1

Apply Mirror to grid cells

• Passes each cell's Mirror setting to its video decoder or MJPEG renderer and exposes it for debug reporting.

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

RtspClient.javaExpose single-view Mirror state +3/-0

Expose single-view Mirror state

• Adds accessors that pass the slot's Mirror setting through to the RTSP session.

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

RtspSession.javaHold Mirror state in each stream session +6/-0

Hold Mirror state in each stream session

• Stores and exposes the slot's Mirror choice alongside its Turn setting.

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

Tests (1) +47 / -0
GlMirrorTest.javaTest Turn and Mirror orientation transforms +47/-0

Test Turn and Mirror orientation transforms

• Checks edge positions across the four quarter-turn orientations, including mirrored output and the order of Turn then Mirror.

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

Documentation (6) +13 / -6
CHANGELOG.mdAnnounce Mirror in the 2.18.0 release +6/-0

Announce Mirror in the 2.18.0 release

• Adds a release entry describing per-slot mirroring for H.264, H.265, and MJPEG.

CHANGELOG.md

README.mdDescribe Mirror in English setup instructions +1/-1

Describe Mirror in English setup instructions

• Adds Mirror alongside Turn in the camera setup guidance.

README.md

README.ru.mdDescribe Mirror in Russian setup instructions +1/-1

Describe Mirror in Russian setup instructions

• Adds the left-to-right Mirror option to the translated camera setup guidance.

README.ru.md

README.zh-CN.mdDescribe Mirror in Chinese setup instructions +1/-1

Describe Mirror in Chinese setup instructions

• Adds the left-to-right Mirror option to the translated camera setup guidance.

README.zh-CN.md

debug.mdDocument Mirror in debug statistics +1/-1

Document Mirror in debug statistics

• Documents the conditional 'mirror: true' field for single-view and grid-cell statistics.

docs/debug.md

usage.mdDocument Mirror behavior and rendering +3/-2

Document Mirror behavior and rendering

• Explains where Mirror appears, its order relative to Turn, codec-specific rendering, and its effect on snapshots and screenshots. Adds Mirror to the Settings menu description.

docs/usage.md

Other (1) +2 / -2
build.gradleBump Android release to 2.18.0 +2/-2

Bump Android release to 2.18.0

• Increments versionCode to 43 and versionName to 2.18.0.

android/app/build.gradle

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Settings can leave the old picture transform ✓ Resolved
Description
The generation check in the video worker occurs before createDecoder(), without making the check
and creation atomic. If saving settings advances listenerGen and closes the decoder after that
check but before the old worker creates one, the old worker can install a decoder using the previous
turn or mirror setting, which the new worker then reuses.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R2391-2394]

+                            if (gen != listenerGen) {
+                                framePool.recycle(buffer);
+                                break;
+                            }
Evidence
Saving settings increments the video-worker generation before closing and reconfiguring the shared
client. Decoder installation checks the manager's separate lifecycle generation, not the worker's
listenerGen, and an existing decoder prevents the new worker from creating its own.

android/app/src/main/java/com/openipc/decoder/Decoder.java[727-745]
android/app/src/main/java/com/openipc/decoder/Decoder.java[605-616]
android/app/src/main/java/com/openipc/decoder/Decoder.java[2388-2397]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[476-486]

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 old video worker can pass the generation check, then create the decoder after settings restart has begun, leaving its old transform in use.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[2388-2397]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[727-745]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[476-486]
## Recommended Fix
Pass the stream generation or immutable stream settings into decoder creation and reject installation if that generation is no longer current. Ensure the check governing installation is coordinated with restart, rather than checking only before creation.

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



Remediation recommended

2. Repeated mirror restarts retain EGL state ✓ Resolved
Description
GlMirror.setUp() initializes the default EGL display for each mirror instance, but tearDown()
deliberately omits the matching eglTerminate() call. Repeated decoder restarts therefore
accumulate unmatched display initializations even though the instances' contexts and windows are
destroyed.
Code

android/app/src/main/java/com/openipc/decoder/GlMirror.java[R273-276]

+            if (window != EGL14.EGL_NO_SURFACE) EGL14.eglDestroySurface(display, window);
+            if (context != EGL14.EGL_NO_CONTEXT) EGL14.eglDestroyContext(display, context);
+            EGL14.eglReleaseThread();
+            // No eglTerminate(): the display is shared with the other mirrored cells
Evidence
The setup and teardown code show an eglInitialize() call per instance and no matching
eglTerminate(); releasing thread-local EGL state does not balance display initialization. Decoder
closure and recreation repeat that pair of paths.

android/app/src/main/java/com/openipc/decoder/GlMirror.java[172-176]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[267-280]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[348-369]

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

## Issue description
Every mirror instance initializes the default EGL display, but releasing that instance does not balance its initialization.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/GlMirror.java[172-176]
- android/app/src/main/java/com/openipc/decoder/GlMirror.java[267-280]
## Recommended Fix
Balance each successful display initialization with a termination after destroying that instance's surface and context, using shared-display coordination if needed to keep other mirrored cells active. Cover partial setup failures as well as normal teardown.

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


3. A codec cleanup failure strands the mirror ✓ Resolved
Description
The configure/start failure handler calls local.release() before releaseMirror(gl) without
protecting the latter in a finally block. If codec release throws after GL setup succeeded, the
outer handler schedules a retry but leaves the GL thread and EGL window attached to the view
surface.
Code

android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[R459-460]

+                releaseMirror(gl);
+                gl = null;
Evidence
GL is created before codec configuration. In the failure handler, local.release() precedes the
only call that releases this local mirror; an exception jumps to the outer handler, which releases
the budget slot but has no mirror-cleanup call.

android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[432-460]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[463-474]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[256-283]

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 throwing codec release skips mirror cleanup when decoder configuration or startup fails.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[453-474]
## Recommended Fix
Place mirror cleanup in a `finally` block around the codec-release attempt, preserving the original setup failure for logging and retry. Ensure the codec is released before beginning GL teardown even when either cleanup operation fails.

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


4. Closing a mirrored slot can freeze the screen for 2 s 🐞 Bug ☼ Reliability
Description
closeDecoderLocked() calls releaseMirror() while holding decoderLock, and GlMirror.release()
waits up to two seconds for GL teardown but treats a timeout as completion without confirming that
tearDown() destroyed the EGL window. When the GL thread stalls in frame processing or
eglSwapBuffers, UI-thread surface callbacks wait for each mirrored cell, and a timed-out close can
let surfaceDestroyed() return or decoder replacement proceed while the old EGL window still owns
the surface.
Code

android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[R360-369]

+            GlMirror gl = takeMirror();
      Log.i(TAG, "Close video decoder");
      try {
          codec.stop();
          codec.release();
      } catch (Exception e) {
          Log.e(TAG, "Decoder close exception", e);
      }
+            releaseMirror(gl);
      releaseSlot(slot);
Evidence
closeDecoderLocked() releases the mirror inside synchronized (decoderLock), and the
surface-destruction callback calls closeDecoder() on the UI thread. GlMirror.release() uses
runSync(this::tearDown), whose latch wait times out after two seconds; release() catches the
timeout and requests thread shutdown, although only tearDown() destroys the EGL window. Meanwhile,
onFrameAvailable() calls eglSwapBuffers() synchronously, where #76 observed a 40-second
compositor stall, so the GL thread may not execute teardown before the bounded wait ends.

android/app/src/main/java/com/openipc/decoder/Decoder.java[275-281]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[139-170]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[349-371]
android/app/src/main/java/com/openipc/decoder/Decoder.java[204-211]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[348-369]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[138-170]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[224-253]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[256-283]

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

## Issue description
Closing a mirrored decoder can hold `decoderLock` and block a UI-thread surface callback while waiting for GL teardown. A timeout does not establish that the old EGL window has been destroyed, so surface destruction or decoder replacement can proceed while it still owns the surface.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[348-371]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[204-211]
- android/app/src/main/java/com/openipc/decoder/GlMirror.java[138-170]
- android/app/src/main/java/com/openipc/decoder/GlMirror.java[256-283]
## Recommended Fix
Detach the decoder promptly and move `releaseMirror(gl)` outside `synchronized (decoderLock)`, preserving codec-then-GL teardown order without making the UI callback wait for the full GL teardown. Set a volatile `released` flag before posting `tearDown` so `onFrameAvailable()` stops calling `eglSwapBuffers`. Track actual teardown completion separately from any bounded caller wait, log an error if it does not finish, and prevent a replacement decoder from taking the target surface until the prior EGL window has been destroyed. Consider a shorter wait bound for the `surfaceDestroyed` path without treating a timeout as successful release.

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


View review recommended (1)
5. Debug stats say mirrored when the picture is not ✓ Resolved
Description
When GlMirror.create throws, createDecoder logs a warning and configures the decoder straight
onto the surface, so the picture is shown unmirrored for that decoder's whole life with no retry.
This includes a passing failure such as the surface still being held by a decoder that is being
released. debugState() still puts mirror: true from the slot setting (rtspClient.mirror() /
cell.mirror()), and the new isMirrored() and framesDrawn() that would show the real state are
never called.
Code

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

+            if (rtspClient != null && rtspClient.mirror()) single.put("mirror", true);
Evidence
The fallback in createDecoder sets gl = null and goes on unmirrored. mirrored is tracked, but
nothing outside MediaCodecManager calls isMirrored() (grep finds only its definition), and
framesDrawn() is never called. /stats.json reads the setting, not what the decoder actually does.

android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[432-445]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[283-284]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1633-1637]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1679-1683]

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

## Issue description
/stats.json reports `mirror: true` from the setting even when the GL path failed and the decoder draws unmirrored. `isMirrored()` and `framesDrawn()` have no callers.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1633-1637]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1679-1683]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[283-284]
## Recommended Fix
For H.264/H.265, report `mirror` from `videoDecoder.isMirrored()` (and the cell decoder's equivalent through QuadCell). Alternatively, add a `mirror_failed` field when the setting is on but the decoder is not mirrored. Consider counting a GL setup failure as a decoder failure so the existing back-off retries it, rather than keeping an unmirrored decoder.

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



Informational

6. GL draw failures go unlogged and unreported ✓ Resolved
Description
GlMirror.onFrameAvailable() ignores the boolean returned by eglSwapBuffers() and leaves its
per-frame eglQuerySurface() calls unchecked, relying instead on a RuntimeException catch for EGL
failures. If the EGL window breaks or the view surface is abandoned after setup, frames can stop
appearing without a warning or recovery while decoder frame counts and fps keep rising and
mirror_failed remains false in /stats.json.
Code

android/app/src/main/java/com/openipc/decoder/GlMirror.java[R250-253]

+            EGL14.eglSwapBuffers(display, window);
+        } catch (RuntimeException e) {
+            // The surface went away meanwhile (view or activity change): the owner releases us
+            Log.w(TAG, "Mirror frame: " + e.getMessage());
Evidence
GlMirror.onFrameAvailable() discards the swap result and catches only RuntimeException, although
setUp() in the same file checks EGL return values and uses eglError(). MediaCodecManager marks
the path mirrored when setup succeeds and counts codec output independently of GL presentation; its
mirrorFailed() check only detects setup fallback, not presentation failures that occur later.

android/app/src/main/java/com/openipc/decoder/GlMirror.java[225-255]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[193-198]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[283-284]
android/app/src/main/java/com/openipc/decoder/GlMirror.java[225-254]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[278-284]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[489-499]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[617-630]

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

## Issue description
`GlMirror.onFrameAvailable()` ignores EGL presentation failures after setup, allowing the mirror to stop updating while codec output continues to be counted and `/stats.json` reports no mirror failure.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/GlMirror.java[225-255]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[278-295]
## Recommended Fix
Check the results of `eglSwapBuffers()` and `eglQuerySurface()`. On failure, log a warning once or at a limited rate that includes `eglError()`, and record the failure in a volatile `drawFailed` flag exposed through a getter. Have `MediaCodecManager.mirrorFailed()` include that state so `/stats.json` reports it, and stop or recreate the failed rendering path so playback can recover or fall back rather than continuing to count invisible frames.

ⓘ 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 open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java
Comment thread android/app/src/main/java/com/openipc/decoder/GlMirror.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
… stats (#226)

- A decoder made by a video thread whose stream was restarted with new
  settings is dropped when installed, not only before creation.
- The GL path is released even when releasing a failed codec throws.
- A queued frame is no longer drawn once the GL path is being released.
- /stats.json: mirror_failed: true when Mirror is on but the GL path
  failed and the decoder draws unmirrored; unused framesDrawn removed.
…226)

Android counts eglInitialize per display, so eglTerminate only drops this
instance's reference; other mirrored cells keep the display.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4d046c3

…ror_failed (#226)

eglQuerySurface and eglSwapBuffers results are checked; the first failure
after setup is logged with the EGL error, and /stats.json reports
mirror_failed for that decoder.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

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

@compot-dev
compot-dev merged commit fd3eba1 into v2 Oct 20, 2026
7 checks passed
@compot-dev
compot-dev deleted the feat/mirror-226 branch October 20, 2026 08:55
compot-dev added a commit that referenced this pull request Oct 20, 2026
Mirror (#246) merged first: its GL path is released on the new failure and
replace paths (failDecoder, dropDecoder); version 2.18.1, versionCode 44.
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.

Per-slot Mirror in the camera settings (needs a GL or TextureView path)

1 participant