From 8ecdea0b95efd83742074160ae29368f6584ad05 Mon Sep 17 00:00:00 2001
From: iflyhere <57563846+iflyhere@users.noreply.github.com>
Date: Thu, 27 Aug 2026 20:23:59 +0200
Subject: [PATCH 1/2] Render the main video on a SurfaceView unless object
detection needs the TextureView
#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-#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.
---
.../com/openipc/pixelpilot/VideoActivity.java | 32 ++++++++++++++++++-
app/src/main/res/layout/activity_video.xml | 14 ++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
index 0c17d62f..f4c0760a 100644
--- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
+++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
@@ -134,6 +134,9 @@ public void run() {
private Timer recordTimer = null;
private int seconds = 0;
private boolean isVRMode = false;
+ // Which view the main video is rendered into. SurfaceView is the low latency
+ // default; the TextureView is only used when object detection needs getBitmap().
+ private boolean videoUsesTextureView = false;
private ConstraintLayout constraintLayout;
private ConstraintSet constraintSet;
private WfbNgLink wfbLink;
@@ -395,6 +398,7 @@ private void initializeVideoPlayers() {
*/
private void setupVRVideoPlayers() {
binding.mainVideo.setVisibility(View.GONE);
+ binding.mainVideoSurface.setVisibility(View.GONE);
binding.surfaceViewLeft.getHolder().addCallback(videoPlayer.configure1(0));
binding.surfaceViewRight.getHolder().addCallback(videoPlayer.configure1(1));
}
@@ -405,7 +409,22 @@ private void setupVRVideoPlayers() {
private void setupStandardVideoPlayer() {
binding.surfaceViewRight.setVisibility(View.GONE);
binding.surfaceViewLeft.setVisibility(View.GONE);
- binding.mainVideo.setSurfaceTextureListener(videoPlayer.configureTextureView(0));
+
+ // Object detection reads frames back with TextureView.getBitmap(), which forces
+ // the video through the view hierarchy's GPU composition. Without it a
+ // SurfaceView is used so the video stays on a hardware overlay plane.
+ videoUsesTextureView = getSharedPreferences("general", MODE_PRIVATE)
+ .getBoolean("od_enabled", false);
+
+ if (videoUsesTextureView) {
+ binding.mainVideoSurface.setVisibility(View.GONE);
+ binding.mainVideo.setVisibility(View.VISIBLE);
+ binding.mainVideo.setSurfaceTextureListener(videoPlayer.configureTextureView(0));
+ } else {
+ binding.mainVideo.setVisibility(View.GONE);
+ binding.mainVideoSurface.setVisibility(View.VISIBLE);
+ binding.mainVideoSurface.getHolder().addCallback(videoPlayer.configure1(0));
+ }
}
// ----------------------------------------------------------------------------
@@ -1602,6 +1621,7 @@ public void onVideoRatioChanged(final int videoW, final int videoH) {
Log.d(TAG, "Set resolution: " + videoW + "x" + videoH);
updateViewRatio(R.id.mainVideo, lastVideoW, lastVideoH);
+ updateViewRatio(R.id.mainVideoSurface, lastVideoW, lastVideoH);
updateViewRatio(R.id.surfaceViewLeft, lastVideoW, lastVideoH);
updateViewRatio(R.id.surfaceViewRight, lastVideoW, lastVideoH);
}
@@ -2045,6 +2065,15 @@ private void setObjectDetectionEnabled(boolean enabled) {
isObjectDetectionEnabled = enabled;
prefs.edit().putBoolean("od_enabled", enabled).apply();
+ // Enabling / disabling detection swaps the main video renderer. Handing the
+ // decoder a different surface at runtime would need the receiver lifecycle in
+ // VideoPlayer reworked, so restart instead - same as the VR mode toggle does.
+ if (!isVRMode && enabled != videoUsesTextureView) {
+ Toast.makeText(this, "Restarting to switch video renderer...", Toast.LENGTH_SHORT).show();
+ resetApp();
+ return;
+ }
+
if (enabled) {
binding.detectionOverlay.setVisibility(View.VISIBLE);
startObjectDetectionLoop();
@@ -2064,6 +2093,7 @@ private void restartObjectDetector() {
private void startObjectDetectionLoop() {
if (isVRMode) return; // Standard mode only
+ if (!videoUsesTextureView) return; // getBitmap() needs the TextureView renderer
if (objectDetectionExecutor == null) {
objectDetectionExecutor = Executors.newSingleThreadExecutor();
}
diff --git a/app/src/main/res/layout/activity_video.xml b/app/src/main/res/layout/activity_video.xml
index cabe29e6..8aa7e519 100644
--- a/app/src/main/res/layout/activity_video.xml
+++ b/app/src/main/res/layout/activity_video.xml
@@ -5,10 +5,24 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+
+
Date: Wed, 2 Sep 2026 22:41:32 +0200
Subject: [PATCH 2/2] Pick the renderer on the same condition that decides
whether detection 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.
---
.../com/openipc/pixelpilot/VideoActivity.java | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
index f4c0760a..a3588f17 100644
--- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
+++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
@@ -413,8 +413,16 @@ private void setupStandardVideoPlayer() {
// Object detection reads frames back with TextureView.getBitmap(), which forces
// the video through the view hierarchy's GPU composition. Without it a
// SurfaceView is used so the video stays on a hardware overlay plane.
+ //
+ // The preference alone is not enough: setObjectDetectionEnabled() turns detection
+ // back off in onResume when the runtime or the selected model is missing, and returns
+ // before the renderer swap - which would leave the session on the TextureView with
+ // nothing reading from it. Both checks are cheap when od_enabled is false, and when
+ // it is true the runtime check only loads a library that is about to be used anyway.
videoUsesTextureView = getSharedPreferences("general", MODE_PRIVATE)
- .getBoolean("od_enabled", false);
+ .getBoolean("od_enabled", false)
+ && isObjectDetectionRuntimeSupported()
+ && isSelectedObjectDetectionModelAvailable();
if (videoUsesTextureView) {
binding.mainVideoSurface.setVisibility(View.GONE);
@@ -2063,7 +2071,10 @@ private void setObjectDetectionEnabled(boolean enabled) {
}
isObjectDetectionEnabled = enabled;
- prefs.edit().putBoolean("od_enabled", enabled).apply();
+ // commit(), not apply(): the restart below ends the process with System.exit()
+ // before an asynchronous write would be flushed, and the renderer picked on the
+ // next launch is read from exactly this value.
+ prefs.edit().putBoolean("od_enabled", enabled).commit();
// Enabling / disabling detection swaps the main video renderer. Handing the
// decoder a different surface at runtime would need the receiver lifecycle in