From cab045a3b3169a7de5450dbca29909a03b53f10b Mon Sep 17 00:00:00 2001 From: Ihor Ivlev Date: Wed, 2 Sep 2026 21:25:12 +0200 Subject: [PATCH 1/2] Restart the app when the low latency toggle changes The low-latency keys from #113 are written into the AMediaFormat when the codec is configured, and the codec is only torn down when its surface goes away. VideoPlayer stop()/start() recycle the UDP receivers and leave the codec alone, and a channel change never touches the surface. So the toast "applies on next video start" promised something that only happened after backgrounding the app or restarting it; a pilot who turned the option off in the field and changed channel kept flying on the old configuration. Do what the VR mode toggle does and restart the app. The preference is read on startup, so the runtime setLowLatency() call in the click handler goes away. Two leftovers from #113 in the same area: setLowLatency() now runs verifyApplicationThread() like every other VideoPlayer setter, and the commented-out AMEDIAFORMAT_KEY_PRIORITY line in h264_configureAMediaFormat is dropped, since writeAndroidPerformanceParams() sets the real key. --- .../main/java/com/openipc/pixelpilot/VideoActivity.java | 9 ++++++--- .../src/main/cpp/helper/AndroidMediaFormatHelper.h | 1 - .../main/java/com/openipc/videonative/VideoPlayer.java | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java index 7261f9ac..c8382849 100644 --- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java +++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java @@ -685,6 +685,11 @@ private void setupBandwidthSubMenu(PopupMenu popup) { * "Low latency" sets the MediaCodec low-latency and realtime-priority keys. It is on * by default; decoders that misbehave with those keys can be put back on the stock * pipeline here. + * + * The keys are only applied when the codec is configured, and the codec is only torn + * down when its surface goes away, not on a channel change or on VideoPlayer + * stop()/start(). So the toggle restarts the app, the same way the VR mode toggle + * does, instead of promising an "on next video start" that never comes. */ private void setupVideoSubMenu(PopupMenu popup) { SubMenu videoMenu = popup.getMenu().addSubMenu("Video"); @@ -697,11 +702,9 @@ private void setupVideoSubMenu(PopupMenu popup) { item.setChecked(enabled); getSharedPreferences("general", MODE_PRIVATE).edit() .putBoolean("low_latency_decoder", enabled).apply(); - videoPlayer.setLowLatency(enabled); - Toast.makeText(this, "Low latency " + (enabled ? "enabled" : "disabled") - + ", applies on next video start.", Toast.LENGTH_SHORT).show(); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); item.setActionView(new View(this)); + resetApp(); return false; }); } diff --git a/app/videonative/src/main/cpp/helper/AndroidMediaFormatHelper.h b/app/videonative/src/main/cpp/helper/AndroidMediaFormatHelper.h index bc3ffa9d..1cda3190 100644 --- a/app/videonative/src/main/cpp/helper/AndroidMediaFormatHelper.h +++ b/app/videonative/src/main/cpp/helper/AndroidMediaFormatHelper.h @@ -37,7 +37,6 @@ static void h264_configureAMediaFormat(KeyFrameFinder& kff, AMediaFormat* format // AMediaFormat_setInt32(format,AMEDIAFORMAT_KEY_FRAME_RATE,60); // AVCProfileBaseline==1 // AMediaFormat_setInt32(decoder.format,AMEDIAFORMAT_KEY_PROFILE,1); - // AMediaFormat_setInt32(decoder.format,AMEDIAFORMAT_KEY_PRIORITY,0); } static void h265_configureAMediaFormat(KeyFrameFinder& kff, AMediaFormat* format) diff --git a/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java b/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java index e1c49d0f..1545689e 100644 --- a/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java +++ b/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java @@ -133,6 +133,7 @@ public boolean isRunning() { * Takes effect the next time the decoder is configured. */ public void setLowLatency(boolean enabled) { + verifyApplicationThread(); nativeSetLowLatency(nativeVideoPlayer, enabled); } From e83d8852fb7dde6cdbcf73ffb8350fdd2fdd418e Mon Sep 17 00:00:00 2001 From: Ihor Ivlev Date: Wed, 2 Sep 2026 21:30:02 +0200 Subject: [PATCH 2/2] Commit the low latency pref and finalize the DVR before restarting Two review findings on the restart path. The preference was written with apply(), and resetApp() ends the process with System.exit() a few lines later. apply() flushes to disk on a background thread, so the new process could read the old value and come up with the configuration the pilot just switched away from. The VR mode toggle already uses commit() for the same reason; do the same here. resetApp() also killed an active DVR recording without closing the MP4: System.exit() skips every lifecycle callback, and the file is only finalized when the DVR thread exits. stopDvr() joins that thread, so call it first. This covers the VR mode toggle too, which had the same hole. --- .../main/java/com/openipc/pixelpilot/VideoActivity.java | 8 +++++++- 1 file changed, 7 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 c8382849..1dfea649 100644 --- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java +++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java @@ -202,6 +202,10 @@ public static String bytesToHex(byte[] bytes) { } private void resetApp() { + // Finalize an active recording first. System.exit() below skips every lifecycle + // callback, and the MP4 is only closed when the DVR thread exits; stopDvr() joins + // it. No-op when nothing is recording. + stopDvr(); // Restart the app Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName()); if (intent != null) { @@ -700,8 +704,10 @@ private void setupVideoSubMenu(PopupMenu popup) { lowLatencyItem.setOnMenuItemClickListener(item -> { boolean enabled = !item.isChecked(); item.setChecked(enabled); + // commit(), not apply(): resetApp() ends the process with System.exit() + // before an asynchronous write would be flushed. getSharedPreferences("general", MODE_PRIVATE).edit() - .putBoolean("low_latency_decoder", enabled).apply(); + .putBoolean("low_latency_decoder", enabled).commit(); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); item.setActionView(new View(this)); resetApp();