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