Skip to content

feat: per-slot Turn in the camera settings, H.264/H.265 turned by the decoder (#201) - #225

Merged
compot-dev merged 10 commits into
v2from
feat/rotate-201
Oct 17, 2026
Merged

compot-dev merged 10 commits into
v2from
feat/rotate-201

Conversation

@compot-dev

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

Copy link
Copy Markdown
Collaborator

Part of #201. Version 2.17 (merges before #220, which becomes 2.18). Mirror moved to #226.

  • Turn in the camera settings (Auto, 0°, 90° ↺, 180°, 270° ↺), stored per slot; Auto keeps the URL's ?rotate= and the toys' defaults.
  • H.264/H.265 are turned by the decoder (MediaFormat.KEY_ROTATION, Android 6+), MJPEG by the hardware JPEG decoder the same way, or while drawing on the software path. ?rotate= now works for every codec.
  • The single view fits the turned size (a quarter turn swaps the sides); grid cells fill the cell as before.
  • Snapshots and long-press screenshots copy the surface as it is: PixelCopy already applies the decoder's turn, so no extra turn (056c794).

Checked on Aceline-1 (CI build 056c794, 1080p H.265 camera):

  • single view at 0/90/180/270: picture on screen, /snapshot.jpg and the long-press screenshot (1080×1920 for 90/270) all have the same orientation;
  • grid: the turned cell and its snapshot match; the settings dialog works with the remote and touch; no frame rate cost.

Not here: Mirror — setScaleX(-1) on the SurfaceView does not mirror on the AG-216, needs a GL or TextureView path: #226.

  • Snapshot and long-press screenshot follow the turn
  • Aceline-1: 0/90/180/270 in the single view and the grid

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

Copy link
Copy Markdown

PR Summary by Qodo

Add per-slot camera rotation across video codecs

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add a saved Turn setting that overrides URL rotation and toy-camera defaults per slot.
• Rotate H.264, H.265, and MJPEG output; fit turned video in single view and grid.
• Preserve the displayed orientation in snapshots and screenshots; document behavior and release
 version 2.18.
Diagram

graph TD
  Settings["Turn setting"] --> Preferences[("Slot preferences")] --> Session["RTSP session"] --> Decoders["Video decoders"] --> Surface["Video surface"] --> Capture["PixelCopy capture"]
  Session --> Layout["View sizing"] --> Surface
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Rotate all codecs in a shared GL rendering path
  • ➕ Could support H.264/H.265 rotation on older Android versions and provide a path for mirroring.
  • ➖ Requires a larger rendering redesign and may add processing cost to every stream.

Recommendation: Keep decoder-assisted rotation and the existing MJPEG software fallback for this PR: they reuse the current rendering paths without a separate compositor. Consider GL separately if older-device rotation or mirroring becomes a requirement.

Files changed (16) +107 / -38

Enhancement (8) +97 / -29
Decoder.javaPersist and apply rotation in single view and grid +28/-7

Persist and apply rotation in single view and grid

• Loads and saves each slot's Turn setting, passes it to single-view and grid sessions, and configures single-view decoder rotation. Swaps reported dimensions for quarter turns and exposes rotation for non-MJPEG streams in debug state.

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

JpegCodec.javaConfigure hardware JPEG decoder rotation +12/-3

Configure hardware JPEG decoder rotation

• Sets MediaFormat rotation on Android 6+ and recreates the JPEG decoder when the requested turn changes.

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

JpegRenderer.javaUse hardware rotation for MJPEG when supported +7/-4

Use hardware rotation for MJPEG when supported

• Allows turned MJPEG through the hardware decoder on Android 6+ and reports its turned dimensions. Older devices retain software drawing for turned frames.

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

MediaCodecManager.javaRotate H.264 and H.265 decoder output +12/-0

Rotate H.264 and H.265 decoder output

• Accepts a counter-clockwise rotation setting and applies the equivalent MediaFormat rotation when creating a decoder on Android 6+.

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

MenuManager.javaAdd Turn choices to camera settings +19/-1

Add Turn choices to camera settings

• Adds Auto, 0°, 90°, 180°, and 270° radio choices to the URL dialog and records the selected value on save.

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

QuadCell.javaApply slot rotation in grid cells +4/-2

Apply slot rotation in grid cells

• Passes the slot's Turn setting into its session and configures H.264/H.265 decoder rotation when a grid decoder starts. Rotation reporting now covers all codecs.

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

RtspClient.javaExpose the slot Turn setting to RTSP sessions +1/-0

Expose the slot Turn setting to RTSP sessions

• Adds a client method that forwards the per-slot rotation override to the session.

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

RtspSession.javaResolve rotation overrides for every codec +14/-12

Resolve rotation overrides for every codec

• Makes the slot setting take precedence over URL rotation and toy defaults, with Auto preserving existing behavior. Removes the MJPEG-only restriction on session rotation.

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

Documentation (7) +8 / -7
README.mdMention Turn in English setup instructions +1/-1

Mention Turn in English setup instructions

• Adds the Turn control to the camera setup guidance for sideways or upside-down cameras.

README.md

README.ru.mdMention Turn in Russian setup instructions +1/-1

Mention Turn in Russian setup instructions

• Adds the Turn control to the Russian camera setup guidance.

README.ru.md

README.zh-CN.mdMention Turn in Chinese setup instructions +1/-1

Mention Turn in Chinese setup instructions

• Adds the Turn control to the Chinese camera setup guidance.

README.zh-CN.md

debug.mdClarify debug rotation reporting +1/-1

Clarify debug rotation reporting

• Describes the debug rotate value as applying to the turned picture rather than only MJPEG frames.

docs/debug.md

toys.mdDocument codec-wide rotation for toy cameras +1/-1

Document codec-wide rotation for toy cameras

• Explains that URL rotation also turns H.264/H.265 from version 2.18 and that the slot Turn setting takes precedence.

docs/toys.md

transports.mdClarify rotation precedence for RTSP slots +1/-1

Clarify rotation precedence for RTSP slots

• Expands URL rotation guidance from MJPEG to all RTSP slots and documents the slot setting's precedence.

docs/transports.md

usage.mdDocument Turn controls and device support +2/-1

Document Turn controls and device support

• Describes the Turn choices, override behavior, supported codecs, capture behavior, and Android version limitation. Adds Turn to the Settings menu description and notes that mirroring is not included.

docs/usage.md

Other (1) +2 / -2
build.gradleSet release version to 2.18 +2/-2

Set release version to 2.18

• Changes the Android version code from 41 to 43 and version name from 2.16 to 2.18.

android/app/build.gradle

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

qodo-free-for-open-source-projects Bot commented Oct 17, 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


Remediation recommended

1. Grid cells show the wrong turned size ✓ Resolved
Description
QuadCell.decode() enables decoder rotation, but width() and height() still read the decoder’s
unrotated output dimensions. For H.264/H.265 streams turned 90° or 270° on Android 6+, the grid’s
status text reports the original width and height even though the displayed picture has its axes
swapped.
Code

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

+            decoder.setRotation(session.rotate());
Evidence
The new grid decode path configures rotation, while the decoder records dimensions directly from its
output format. Unlike the single-view resolution callback, the grid ignores that callback and
exposes the recorded dimensions unchanged in its status text.

android/app/src/main/java/com/openipc/decoder/QuadCell.java[694-700]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[531-541]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[129-132]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[366-377]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[556-559]
android/app/src/main/java/com/openipc/decoder/Decoder.java[214-221]

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

## Issue description
Grid-cell status reports unrotated dimensions for quarter-turned H.264/H.265 video.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[558-559]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[698-699]
## Recommended Fix
When exposing a video decoder’s dimensions, swap width and height for effective 90° or 270° rotation only where decoder rotation is supported. Keep MJPEG dimensions as returned by its renderer, since those are already turned.

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


2. Older Android streams get the wrong fit ✓ Resolved
Description
The single-view resolution callback swaps width and height for quarter turns even when
MediaCodecManager cannot apply decoder rotation. On supported Android 5 devices, a 90° or 270°
setting therefore fits unturned H.264/H.265 video using the opposite aspect ratio.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R218-219]

+                if (r == 90 || r == 270) updateResolution(h, w);
+                else updateResolution(w, h);
Evidence
The app supports API 21, but video decoder rotation is configured only on API 23 and newer. The
resolution callback swaps dimensions regardless of API level, and fitVideo uses those dimensions to
size the surface.

android/app/build.gradle[23-25]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[385-389]
android/app/src/main/java/com/openipc/decoder/Decoder.java[214-220]
android/app/src/main/java/com/openipc/decoder/Decoder.java[2129-2157]

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

## Issue description
On Android API 21–22, H.264/H.265 decoder rotation is unavailable, but the single view still swaps its dimensions for quarter turns and fits the unturned picture to the wrong aspect ratio.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[214-220]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[385-389]
## Recommended Fix
Use the same Android version condition for the single-view dimension swap as for applying decoder rotation. Keep the MJPEG sizing path unchanged because its software renderer can turn frames on older devices.

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


3. Turn choices are clipped on narrow screens ✓ Resolved
Description
showUrlEditor() puts five radio buttons in one horizontal RadioGroup without wrapping or
scrolling. When their combined width exceeds the dialog, the later choices fall outside the visible
content and cannot be selected there.
Code

android/app/src/main/java/com/openipc/decoder/MenuManager.java[R570-573]

+        RadioGroup turn = new RadioGroup(activity);
+        turn.setOrientation(RadioGroup.HORIZONTAL);
+        int[] turns = {-1, 0, 90, 180, 270};
+        String[] turnLabels = {"Auto", "0°", "90° ↺", "180°", "270° ↺"};
Evidence
The group is explicitly horizontal, receives five labeled buttons, and is added directly to padded
dialog content without a wrapping or scrolling container.

android/app/src/main/java/com/openipc/decoder/MenuManager.java[568-587]

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

## Issue description
Five turn choices are placed in a single unscrollable horizontal row and can extend past the dialog on narrow screens.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/MenuManager.java[570-587]
## Recommended Fix
Use a layout that wraps or stacks the choices, or provide a scrollable control that keeps every choice reachable.

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


View review recommended (2)
4. Reconnects can leave video unturned ✓ Resolved
Description
createDecoder() and the grid decode path capture rotate() when creating a codec, but
RtspSession.connect() temporarily resets URL-derived rotation to zero before the new stream
description arrives. If a queued frame creates a decoder during that interval, the later description
restores the rotation value without recreating the H.264/H.265 decoder, so that decoder keeps the
wrong turn.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R611-613]

+        RtspClient c = rtspClient;
+        videoDecoder.setRotation(c != null ? c.rotate() : 0);
videoDecoder.createDecoder(codecH265);
Evidence
Connection setup resets rotation before assigning the URL value at SDP; queued frames can be
consumed meanwhile. Rotation is written into the codec format only at creation, and the
unchanged-video SDP path does not close that codec. A per-slot override masks this race, but Auto
leaves the URL-derived value exposed.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[330-334]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[489-493]
android/app/src/main/java/com/openipc/decoder/RtspClient.java[106-124]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[589-605]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[694-701]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[382-390]

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

## Issue description
Queued frames can create a codec while reconnect has reset URL-derived rotation but has not yet parsed the new stream description.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[330-334]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[609-614]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[694-701]
## Recommended Fix
Prevent decoder creation from old queued frames during negotiation, or recreate an existing codec when the effective rotation established by the new stream description differs from its configured rotation.

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


5. Turn logs can disagree with the picture ✓ Resolved
Description
rotation() logs the URL or toy default, while the new rotate() returns the per-slot override to
rendering and statistics. When the two values differ, the log reports an inactive turn; when only a
per-slot turn is set, it logs no turn at all.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R691-694]

+    int rotate() {
+        int t = slotRotate;
+        return t >= 0 ? t : rotate;
+    }
Evidence
The log is emitted using wanted before rotate() selects slotRotate. Both the decoder and
reported debug rotation read the effective value, so a configured override makes those outputs
disagree with the log.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[679-698]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1603-1630]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[694-700]

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

## Issue description
Rotation logging uses the URL or toy value even when a per-slot setting overrides the displayed turn.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[679-698]
## Recommended Fix
Log the effective rotation after applying the slot override, including a nonzero per-slot turn when the URL has no turn.

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



Informational

6. Android 5 log and stats claim a turn that isn't shown 🐞 Bug ◔ Observability
Description
RtspSession.rotation() logs "video turned N° counter-clockwise" for H.264/H.265 whenever the slot
or URL turn is non-zero, and debugState() now reports rotate from rtspClient.rotate() for
every codec. On Android 5, MediaCodecManager.createDecoder() skips KEY_ROTATION
(appliedRotation = 0), so the picture is not turned while the log line and /stats.json
(rotate, and the docs say size is the turned one) both say it is.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R697-702]

+        if (turn != 0 && !rotateLogged) {
         rotateLogged = true;
-            Log.i(TAG, tag + ": MJPEG frames turned " + wanted + "\u00b0 counter-clockwise"
-                    + " (#200; ?rotate=0 turns it off)");
+            // MJPEG is turned while drawing (#200), H.264/H.265 by the decoder (#201)
+            Log.i(TAG, tag + ": " + (jpeg ? "MJPEG frames" : "video") + " turned " + turn
+                    + "\u00b0 counter-clockwise" + (slot >= 0 ? " (the slot's Turn setting)"
+                    : " (?rotate=0 or the slot's Turn 0\u00b0 turns it off)"));
Evidence
Below Android M, MediaCodecManager sets turn to 0 and appliedRotation to 0, so the video is not
turned. The log in rotation() and the stats in Decoder.debugState() do not check the SDK level or
the decoder's applied turn (the single view's onResolutionChanged does check SDK_INT), so they
report a turn that is not in effect.

android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[392-399]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[694-705]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1629-1631]

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

## Issue description
On Android < 6, H.264/H.265 are not turned (KEY_ROTATION is skipped), but the session log says "video turned N°" and /stats.json reports `rotate`.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[694-705]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1629-1631]
## Recommended Fix
In `rotation()`, when `!jpeg && Build.VERSION.SDK_INT < M`, log that the turn is not applied on this Android version instead of claiming it is. In `debugState()`, report `rotate` for non-MJPEG streams only when `videoDecoder.swapsSides()` is true or the applied rotation is non-zero (for example, expose `appliedRotation` from MediaCodecManager).

ⓘ 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 see how risky a PR is at a glance with blast radius labels

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/MenuManager.java
Comment on lines +611 to 613
RtspClient c = rtspClient;
videoDecoder.setRotation(c != null ? c.rotate() : 0);
videoDecoder.createDecoder(codecH265);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Reconnects can leave video unturned 🐞 Bug ≡ Correctness

createDecoder() and the grid decode path capture rotate() when creating a codec, but
RtspSession.connect() temporarily resets URL-derived rotation to zero before the new stream
description arrives. If a queued frame creates a decoder during that interval, the later description
restores the rotation value without recreating the H.264/H.265 decoder, so that decoder keeps the
wrong turn.
Agent Prompt
## Issue description
Queued frames can create a codec while reconnect has reset URL-derived rotation but has not yet parsed the new stream description.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[330-334]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[609-614]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[694-701]
## Recommended Fix
Prevent decoder creation from old queued frames during negotiation, or recreate an existing codec when the effective rotation established by the new stream description differs from its configured rotation.

ⓘ 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/RtspSession.java
…crolls, the log names the turn in effect (Qodo on #225)
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo findings, all fixed in 22f4d0a (checked locally in compot/android:2026.10: assembleDebug, testDebugUnitTest, lintDebug — green, no new lint warnings):

  1. Turn choices clipped on narrow screens — the Turn row is now in a HorizontalScrollView, every choice stays reachable.
  2. Reconnects can leave video unturned — connect() no longer resets the turn to 0 until the SDP: an RTSP or Jieli connection takes the URL's turn right after parsing it, before any frame; listener, HTTP MJPEG and FLOW-UFO get 0 there as before. A decoder made from a queued frame now gets the same turn as after the SDP.
  3. Turn logs can disagree with the picture — the log line names the turn in effect, with the slot's setting over the URL's, and says where it comes from; a changed slot setting is logged again.

@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 22f4d0a

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo, second round:

  • Older Android streams get the wrong fit — fixed in f2a3af4: the single view swaps the sides for a quarter turn only on Android 6+, the same condition as KEY_ROTATION in MediaCodecManager; MJPEG sizing is unchanged.
  • Reconnects can leave video unturned — fixed in 22f4d0a (the text above still quotes the old connect()): the turn is no longer reset to 0 before the SDP, an RTSP or Jieli connection takes the URL's turn right after parsing it.

v2 merged in (#227). Checked locally in compot/android:2026.10: assembleDebug, testDebugUnitTest, lintDebug — green twice with --rerun-tasks.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo, second round:

  • Older Android streams get the wrong fit — fixed in f2a3af4: the single view swaps the sides for a quarter turn only on Android 6+, the same condition as KEY_ROTATION in MediaCodecManager; MJPEG sizing is unchanged.
  • Reconnects can leave video unturned — fixed in 22f4d0a (the text above still quotes the old connect()): the turn is no longer reset to 0 before the SDP, an RTSP or Jieli connection takes the URL's turn right after parsing it.

v2 merged in (#227). Checked locally in compot/android:2026.10: assembleDebug, testDebugUnitTest, lintDebug — green twice with --rerun-tasks.

Thanks for the update. Based on the commits and validation you provided:

  • finding 1 (older Android sizing) is addressed in f2a3af4.
  • finding 3 (reconnect rotation race) is addressed in 22f4d0a.
  • The reported assembleDebug, testDebugUnitTest, and lintDebug checks pass twice.

The published review may still show those findings as open until it is refreshed against the latest commit.

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

Copy link
Copy Markdown

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

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo, third round:

  • Grid cells show the wrong turned size — fixed in fe08665: MediaCodecManager remembers the turn its decoder really applies (0 below Android 6), and the cell's label swaps the sides for a quarter turn. The load estimates use width × height and need no swap.
  • Reconnects can leave video unturned — fixed in 22f4d0a; the finding still quotes RtspSession.java 330–334 from before that commit, where connect() now sets the URL's turn before any frame instead of resetting it to 0.

Checked locally in compot/android:2026.10: assembleDebug, testDebugUnitTest, lintDebug — green, lint at the same 9 warnings as v2.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

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

Copy link
Copy Markdown

Qodo, third round:

  • Grid cells show the wrong turned size — fixed in fe08665: MediaCodecManager remembers the turn its decoder really applies (0 below Android 6), and the cell's label swaps the sides for a quarter turn. The load estimates use width × height and need no swap.
  • Reconnects can leave video unturned — fixed in 22f4d0a; the finding still quotes RtspSession.java 330–334 from before that commit, where connect() now sets the URL's turn before any frame instead of resetting it to 0.

Checked locally in compot/android:2026.10: assembleDebug, testDebugUnitTest, lintDebug — green, lint at the same 9 warnings as v2.

Thanks for the update. The reported fixes address finding 1 (grid dimensions) and finding 4 (reconnect rotation). The local assembleDebug, testDebugUnitTest, and lintDebug checks are green, with no new lint warnings reported.

Comment on lines +697 to +702
if (turn != 0 && !rotateLogged) {
rotateLogged = true;
Log.i(TAG, tag + ": MJPEG frames turned " + wanted + "\u00b0 counter-clockwise"
+ " (#200; ?rotate=0 turns it off)");
// MJPEG is turned while drawing (#200), H.264/H.265 by the decoder (#201)
Log.i(TAG, tag + ": " + (jpeg ? "MJPEG frames" : "video") + " turned " + turn
+ "\u00b0 counter-clockwise" + (slot >= 0 ? " (the slot's Turn setting)"
: " (?rotate=0 or the slot's Turn 0\u00b0 turns it off)"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

6. Android 5 log and stats claim a turn that isn't shown 🐞 Bug ◔ Observability

RtspSession.rotation() logs "video turned N° counter-clockwise" for H.264/H.265 whenever the slot
or URL turn is non-zero, and debugState() now reports rotate from rtspClient.rotate() for
every codec. On Android 5, MediaCodecManager.createDecoder() skips KEY_ROTATION
(appliedRotation = 0), so the picture is not turned while the log line and /stats.json
(rotate, and the docs say size is the turned one) both say it is.
Agent Prompt
## Issue description
On Android < 6, H.264/H.265 are not turned (KEY_ROTATION is skipped), but the session log says "video turned N°" and /stats.json reports `rotate`.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[694-705]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1629-1631]

## Recommended Fix
In `rotation()`, when `!jpeg && Build.VERSION.SDK_INT < M`, log that the turn is not applied on this Android version instead of claiming it is. In `debugState()`, report `rotate` for non-MJPEG streams only when `videoDecoder.swapsSides()` is true or the applied rotation is non-zero (for example, expose `appliedRotation` from MediaCodecManager).

ⓘ 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 fe08665

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo, fourth round: the five earlier findings are resolved.

  • Android 5 log and stats claim a turn that isn't shown — left as is: on Android 5 (API 21–22) H.264/H.265 cannot be turned by the decoder, and the docs say so (docs/usage.md: "Android 6+; before that only MJPEG is turned"). The log line and rotate in /stats.json give the turn asked for by the URL or the slot; no lab device runs Android 5. Worth a follow-up only if someone reports it.

Final head fe08665 checked on Aceline-1 with the CI build: ?rotate=90 → single view 1080×1920, grid cell labelled 1080×1920; earlier 0/90/180/270 check of snapshots and screenshots (056c794) still holds. CI green.

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

Copy link
Copy Markdown

Qodo, fourth round: the five earlier findings are resolved.

  • Android 5 log and stats claim a turn that isn't shown — left as is: on Android 5 (API 21–22) H.264/H.265 cannot be turned by the decoder, and the docs say so (docs/usage.md: "Android 6+; before that only MJPEG is turned"). The log line and rotate in /stats.json give the turn asked for by the URL or the slot; no lab device runs Android 5. Worth a follow-up only if someone reports it.

Final head fe08665 checked on Aceline-1 with the CI build: ?rotate=90 → single view 1080×1920, grid cell labelled 1080×1920; earlier 0/90/180/270 check of snapshots and screenshots (056c794) still holds. CI green.

Thanks for the update. The five earlier findings are resolved, and finding 6 remains intentionally open as a documented follow-up for Android 5 observability. The reported Aceline-1 validation and green CI support the current implementation.

@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 56790a5

@compot-dev
compot-dev merged commit 5cd6416 into v2 Oct 17, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/rotate-201 branch October 17, 2026 16:16
compot-dev added a commit that referenced this pull request Oct 17, 2026
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