Skip to content

Render the main video on a SurfaceView unless object detection needs the TextureView - #114

Merged
vertexodessa merged 2 commits into
OpenIPC:masterfrom
iflyhere:fix/main-video-surfaceview
Sep 2, 2026
Merged

Render the main video on a SurfaceView unless object detection needs the TextureView#114
vertexodessa merged 2 commits into
OpenIPC:masterfrom
iflyhere:fix/main-video-surfaceview

Conversation

@iflyhere

@iflyhere iflyhere commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Note

Compile tested only (arm64-v8a + armeabi-v7a). Not yet flown. Object detection
itself is unchanged by this PR, but the renderer switch path deserves a check
on hardware.

The problem

#108 changed the primary renderer from a SurfaceView to a TextureView:

-    <SurfaceView
+    <TextureView
         android:id="@+id/mainVideo"

That is needed for the feature — TextureView.getBitmap() is how frames reach
MediaPipe — but the swap is unconditional. Users with object detection off, i.e.
everybody by default, now get the TextureView path too.

The difference is not cosmetic. A SurfaceView gets its own layer and can be
promoted to a hardware overlay plane: the decoder writes into it and the display
controller scans it out. A TextureView is drawn as part of the view hierarchy,
so every decoded frame has to go through GPU composition together with the rest
of the UI. That costs GPU time, power, and roughly one frame of extra latency,
on a receiver whose whole point is low latency.

The change

The layout carries both renderers and the active one is picked from the existing
od_enabled preference:

Object detection Renderer Behaviour
off (default) mainVideoSurface (SurfaceView) pre-#108
on mainVideo (TextureView) unchanged from #108

Details:

  • setupStandardVideoPlayer() selects the view and records the choice in
    videoUsesTextureView
  • setupVRVideoPlayers() hides both, as before
  • onVideoRatioChanged() keeps the aspect ratio of the new view in sync
  • startObjectDetectionLoop() bails out unless the TextureView is the active
    renderer, so getBitmap() is never called on the wrong view

Why the toggle restarts the app

Switching detection on or off means the decoder needs a different surface.
VideoPlayer.stopAndRemoveReceiverDecoder() also calls stop(), which tears
down the UDP receiver, and nothing restarts it — only onResume() calls
start(). So a hot surface swap silently kills reception today. Rather than
rework the receiver lifecycle in this PR, the toggle calls resetApp(), the
same thing the existing VR mode toggle does, and says so with a toast.

Fixing VideoPlayer's receiver/decoder lifecycle so the surface can be swapped
in place would be a good follow-up; it would also let VR mode be toggled without
a restart.


Part of a series of independent fixes found while building an immersive (OpenXR) mode on a
Quest 3, each standalone and mergeable in any order:

#113 and #116 are now confirmed on hardware (Quest 3, Horizon OS, Android 14).

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

Copy link
Copy Markdown

PR Summary by Qodo

Use SurfaceView unless object detection requires TextureView

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Selects SurfaceView by default to restore low-latency, hardware-overlay video rendering.
• Retains TextureView frame capture when object detection is enabled.
• Restarts standard-mode playback when detection toggles to safely replace decoder surfaces.
Diagram

graph TD
  P["OD Preference"] --> A["Video Activity"] --> D{"Detection enabled?"}
  D -- No --> S["SurfaceView"]
  D -- Yes --> T["TextureView"] --> M["MediaPipe"]
  A --> V["VideoPlayer"] --> S
  V --> T
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Hot-swap decoder surfaces
  • ➕ Avoids restarting the application when detection changes
  • ➕ Could also enable seamless VR mode switching
  • ➖ Requires reworking VideoPlayer receiver and decoder lifecycle
  • ➖ Expands native playback risk beyond this performance fix
2. Keep TextureView always active
  • ➕ Preserves the simplest renderer lifecycle
  • ➕ Allows immediate object-detection toggling
  • ➖ Retains unnecessary GPU composition and power usage
  • ➖ Adds latency for users with detection disabled

Recommendation: Keep the PR's preference-driven dual-renderer approach and restart safeguard. It restores the efficient default without destabilizing receiver lifecycle code; pursue hot surface swapping separately with dedicated hardware and lifecycle testing.

Files changed (2) +45 / -1

Enhancement (1) +14 / -0
activity_video.xmlAdd a dedicated low-latency main SurfaceView +14/-0

Add a dedicated low-latency main SurfaceView

• Adds a full-size SurfaceView as the default main renderer while retaining the hidden TextureView for object detection. Both views share the same constrained video area.

app/src/main/res/layout/activity_video.xml

Bug fix (1) +31 / -1
VideoActivity.javaSelect the main renderer from object-detection state +31/-1

Select the main renderer from object-detection state

• Chooses SurfaceView for normal playback and TextureView when object detection requires bitmap capture. It synchronizes aspect ratios, hides both main views in VR mode, restarts after renderer-changing toggles, and prevents detection from running without TextureView.

app/src/main/java/com/openipc/pixelpilot/VideoActivity.java

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Restart can lose toggle 🐞 Bug ☼ Reliability
Description
The new renderer-switch branch calls resetApp() immediately after od_enabled was saved with
asynchronous apply(), and resetApp() terminates the process with System.exit(0). If the disk
write has not completed, the relaunched activity reads the old value and selects the old renderer,
so the user's toggle appears to fail.
Code

app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[R2010-2012]

+        if (!isVRMode && enabled != videoUsesTextureView) {
+            Toast.makeText(this, "Restarting to switch video renderer...", Toast.LENGTH_SHORT).show();
+            resetApp();
Evidence
The setter asynchronously applies od_enabled and the added branch immediately invokes a helper
that kills the process. The equivalent VR toggle explicitly uses commit() before calling that
helper, demonstrating that restart-sensitive settings are persisted synchronously in this codebase.

app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[2004-2013]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[202-210]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[159-164]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[643-648]

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 renderer restart can terminate the process before the asynchronous `od_enabled` preference write reaches disk, causing the app to restart with the old renderer.

## Issue Context
`resetApp()` immediately starts a fresh task and calls `System.exit(0)`. The existing VR restart path uses synchronous `commit()` before invoking the same reset helper.

## Fix Focus Areas
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[2004-2013]
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[202-210]

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



Remediation recommended

2. Disabled detection keeps TextureView 🐞 Bug ➹ Performance
Description
setupStandardVideoPlayer() selects the TextureView solely from the persisted flag before
onResume() validates runtime and model availability. When that validation disables object
detection, it returns without switching or restarting the renderer, leaving the expensive
TextureView active for the entire session even though detection is off.
Code

app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[R389-390]

+        videoUsesTextureView = getSharedPreferences("general", MODE_PRIVATE)
+                .getBoolean("od_enabled", false);
Evidence
Renderer setup reads the raw persisted flag during onCreate, while availability validation happens
later from onResume. The validation failure clears the preference and returns before the newly
added mismatch/reset logic, so the already configured TextureView remains active.

app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[278-293]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[356-366]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[389-400]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[1514-1516]
app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[1988-2001]

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

## Issue description
Startup can select the TextureView from a stale enabled preference and then automatically disable object detection without returning to the SurfaceView.

## Issue Context
This occurs when a previously enabled model is no longer available or the object-detection runtime is unsupported. The early validation failure clears `od_enabled` but does not execute the new renderer-mismatch restart branch.

## Fix Focus Areas
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[389-400]
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[1988-2001]

ⓘ 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 ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +2010 to +2012
if (!isVRMode && enabled != videoUsesTextureView) {
Toast.makeText(this, "Restarting to switch video renderer...", Toast.LENGTH_SHORT).show();
resetApp();

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. Restart can lose toggle 🐞 Bug ☼ Reliability

The new renderer-switch branch calls resetApp() immediately after od_enabled was saved with
asynchronous apply(), and resetApp() terminates the process with System.exit(0). If the disk
write has not completed, the relaunched activity reads the old value and selects the old renderer,
so the user's toggle appears to fail.
Agent Prompt
## Issue description
The renderer restart can terminate the process before the asynchronous `od_enabled` preference write reaches disk, causing the app to restart with the old renderer.

## Issue Context
`resetApp()` immediately starts a fresh task and calls `System.exit(0)`. The existing VR restart path uses synchronous `commit()` before invoking the same reset helper.

## Fix Focus Areas
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[2004-2013]
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[202-210]

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

Comment on lines +389 to +390
videoUsesTextureView = getSharedPreferences("general", MODE_PRIVATE)
.getBoolean("od_enabled", false);

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. Disabled detection keeps textureview 🐞 Bug ➹ Performance

setupStandardVideoPlayer() selects the TextureView solely from the persisted flag before
onResume() validates runtime and model availability. When that validation disables object
detection, it returns without switching or restarting the renderer, leaving the expensive
TextureView active for the entire session even though detection is off.
Agent Prompt
## Issue description
Startup can select the TextureView from a stale enabled preference and then automatically disable object detection without returning to the SurfaceView.

## Issue Context
This occurs when a previously enabled model is no longer available or the object-detection runtime is unsupported. The early validation failure clears `od_enabled` but does not execute the new renderer-mismatch restart branch.

## Fix Focus Areas
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[389-400]
- app/src/main/java/com/openipc/pixelpilot/VideoActivity.java[1988-2001]

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

@vertexodessa

Copy link
Copy Markdown
Collaborator

@iflyhere both Qodo points hold up, and I have nothing to add beyond a bit of detail:

  • resetApp() calls System.exit(0) straight after finish(), so the pause/stop path that flushes a pending apply() never runs. The VR toggle and the low latency toggle in the same file already use commit() for exactly this reason, od_enabled needs the same before the restart.

  • setupStandardVideoPlayer() runs from onCreate and picks the renderer from od_enabled alone. setObjectDetectionEnabled() in onResume can still turn detection off (runtime unsupported, model missing) and returns before your restart branch, so that session ends up on the TextureView with nothing using it. Picking the TextureView only when od_enabled && isObjectDetectionRuntimeSupported() && isSelectedObjectDetectionModelAvailable() would cover it; both helpers just read prefs and files, so calling them from onCreate is fine.

…the TextureView

OpenIPC#108 replaced the main video SurfaceView with a TextureView so MediaPipe can
grab frames via getBitmap(). That swap is unconditional, so every user pays
for it even with object detection turned off: the video is no longer eligible
for a hardware overlay plane and instead goes through the view hierarchy's GPU
composition, which costs GPU time, power and about one frame of latency.

The layout now carries both renderers and the active one is picked from the
existing "od_enabled" preference:

- object detection off (default) -> mainVideoSurface (SurfaceView), the
  pre-OpenIPC#108 behaviour
- object detection on            -> mainVideo (TextureView), unchanged

Toggling detection in the menu swaps the renderer, which means the decoder
needs a different surface. VideoPlayer.stopAndRemoveReceiverDecoder() also
stops the UDP receiver and nothing restarts it, so a hot swap is not safe
today; the toggle restarts the app instead, the same way the VR mode toggle
already does. startObjectDetectionLoop() bails out if the TextureView is not
the active renderer.
@iflyhere
iflyhere force-pushed the fix/main-video-surfaceview branch from a3d4002 to 22e3015 Compare September 2, 2026 20:41
…n runs

Two fixes from review.

setupStandardVideoPlayer() chose the renderer from od_enabled alone, but that preference is
not the same thing as "detection is going to run". setObjectDetectionEnabled() in onResume
turns it back off when the runtime or the selected model is missing, and returns before
reaching the renderer swap - so a device that cannot do detection at all still spent the whole
session on the TextureView, paying for GPU composition that nothing read from. It only
corrected itself on the next launch, because by then the preference had been written back as
false. The renderer now checks the runtime and the model too.

Both extra calls are behind od_enabled, so nothing changes for anyone with detection off; when
it is on, isObjectDetectionRuntimeSupported() loads a library that onResume was about to load
a few milliseconds later anyway, and caches the result.

The od_enabled write before the restart also used apply(). resetApp() calls System.exit(0)
straight after finish(), so the pause/stop path that would flush an asynchronous write never
runs, and the renderer picked on the next launch is read from exactly this value - a lost
write means the app restarts into the renderer it was trying to leave. commit(), the same as
the VR and low latency toggles in this file.
@iflyhere

iflyhere commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Both fixed and pushed, rebased onto master (so it now sits on top of #121).

1. commit() for od_enabled, with the same reasoning as the low latency toggle a few
hundred lines up. The consequence is worse than a lost preference, which is worth noting: the
value that gets lost is the one the next launch reads to pick the renderer, so the app
restarts straight back into the renderer it was trying to leave.

The apply() in the unsupported/missing-model branch is left alone — that path returns
without a restart, so there is nothing to race.

2. The renderer now checks all three:

videoUsesTextureView = prefs.getBoolean("od_enabled", false)
        && isObjectDetectionRuntimeSupported()
        && isSelectedObjectDetectionModelAvailable();

You are right that it self-corrected on the next launch, since setObjectDetectionEnabled()
writes od_enabled back as false — but the session it happened in ran on the TextureView with
nothing reading from it, which is the case this PR exists to avoid.

One correction to "both helpers just read prefs and files":
isObjectDetectionRuntimeSupported() also does a Class.forName and a System.loadLibrary.
It doesn't change the conclusion, because od_enabled short-circuits ahead of it — with
detection off nothing extra runs, and with it on that library was going to be loaded in
onResume a few milliseconds later anyway, and the result is cached either way. Just worth
having the order of those && be deliberate rather than incidental.

Compile tested for arm64-v8a + armeabi-v7a.

@vertexodessa
vertexodessa merged commit 973f280 into OpenIPC:master Sep 2, 2026
@vertexodessa

Copy link
Copy Markdown
Collaborator

Thank you! merged.

@iflyhere
iflyhere deleted the fix/main-video-surfaceview branch September 3, 2026 17:42
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.

2 participants