From 3db4c073d8daa12bf1efa0b916a036515ab5e224 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 19:09:22 +0000 Subject: [PATCH 1/3] feat: the ninth multi-view cell explains the reserved UDP RTP slot (#7) The 3x3 grid has nine cells for eight slots. The ninth is reserved for a camera that pushes RTP over UDP; until the receiver exists it shows the port (5600) and this device's address to send to, instead of a black square. Test for the hint text; READMEs mention the reserved cell. --- README.md | 2 +- README.ru.md | 2 +- README.zh-CN.md | 2 +- .../java/com/openipc/decoder/Decoder.java | 46 ++++++++++++++++++- .../com/openipc/decoder/QuadGridTest.java | 10 ++++ 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f5c5eda3..227c705d 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ The app accepts plain-text shares (`ACTION_SEND`, `text/plain`). Sharing an `rts ### Multi-view (Quad) -Shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. Each cell has its own connection and decoder, so the device's video decoder limits how many high-resolution streams play smoothly: with five or more cameras, their substreams are a good choice. Some cameras accept only one RTSP client at a time. Long-press a cell to play its audio (long-press it again to mute); the selected cell shows an AUDIO badge and is remembered. Tap any cell to open the menu. +Shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. The ninth cell of the 3×3 grid is reserved for a camera that pushes RTP over UDP (planned, [#7](https://github.com/OpenIPC/decoder/issues/7)); for now it shows the address and port to send to. Each cell has its own connection and decoder, so the device's video decoder limits how many high-resolution streams play smoothly: with five or more cameras, their substreams are a good choice. Some cameras accept only one RTSP client at a time. Long-press a cell to play its audio (long-press it again to mute); the selected cell shows an AUDIO badge and is remembered. Tap any cell to open the menu. ## Known limitations diff --git a/README.ru.md b/README.ru.md index 7a73ab36..b358a6d0 100644 --- a/README.ru.md +++ b/README.ru.md @@ -81,7 +81,7 @@ ### Мультивид (Quad) -Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. У каждой ячейки своё подключение и свой декодер, поэтому число плавно идущих потоков высокого разрешения ограничено видеодекодером устройства: для пяти и более камер лучше брать их дополнительные потоки (substream). Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. +Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3 зарезервирована под камеру, которая сама отправляет RTP по UDP (в планах, [#7](https://github.com/OpenIPC/decoder/issues/7)); пока в ней показаны адрес и порт, куда слать поток. У каждой ячейки своё подключение и свой декодер, поэтому число плавно идущих потоков высокого разрешения ограничено видеодекодером устройства: для пяти и более камер лучше брать их дополнительные потоки (substream). Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. ## Известные ограничения diff --git a/README.zh-CN.md b/README.zh-CN.md index 34c8bd11..3aa53a07 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -81,7 +81,7 @@ ### 多画面模式(Quad) -以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。每个画面都有独立的连接和解码器,因此能流畅播放多少路高分辨率码流取决于设备的视频解码器:五路及以上时建议使用摄像机的子码流。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 +以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格预留给通过 UDP 主动推送 RTP 的摄像机(计划中,[#7](https://github.com/OpenIPC/decoder/issues/7));目前其中显示推流目标地址和端口。每个画面都有独立的连接和解码器,因此能流畅播放多少路高分辨率码流取决于设备的视频解码器:五路及以上时建议使用摄像机的子码流。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 ## 已知限制 diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index 828f18f1..10121c13 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -44,8 +44,12 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.concurrent.ArrayBlockingQueue; @@ -567,8 +571,9 @@ private void buildQuadGrid(int size) { for (int col = 0; col < size; col++) { int idx = row * size + col; if (idx >= CAM_COUNT) { - // The last cell of a 3x3 grid: no slot, stays black - lr.addView(new View(this), new LinearLayout.LayoutParams( + // The ninth cell of a 3x3 grid is reserved for a pushed UDP RTP stream + // (#7); until the receiver exists it explains what will appear there + lr.addView(udpHintCell(), new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, 1)); continue; } @@ -605,6 +610,43 @@ private void buildQuadGrid(int size) { } } + /** Port of the reserved UDP RTP cell (#7); 5600 is the usual default for pushed RTP. */ + static final int UDP_RTP_PORT = 5600; + + private View udpHintCell() { + TextView hint = new TextView(this); + hint.setText(udpHintText(localIpv4(), UDP_RTP_PORT)); + hint.setTextColor(Color.GRAY); + hint.setTextSize(13); + hint.setGravity(Gravity.CENTER); + hint.setPadding(dp(12), dp(12), dp(12), dp(12)); + hint.setBackgroundColor(Color.BLACK); + return hint; + } + + static String udpHintText(String ip, int port) { + String target = (ip != null ? ip : "this device") + ":" + port; + return "UDP RTP \u00b7 port " + port + "\nWaiting for a stream\u2026\n\n" + + "Send the camera's RTP/UDP output to\n" + target; + } + + /** First site-local IPv4 address of this device, or null. */ + static String localIpv4() { + try { + for (NetworkInterface nif : Collections.list(NetworkInterface.getNetworkInterfaces())) { + if (!nif.isUp() || nif.isLoopback()) continue; + for (InetAddress a : Collections.list(nif.getInetAddresses())) { + if (a instanceof Inet4Address && a.isSiteLocalAddress()) { + return a.getHostAddress(); + } + } + } + } catch (Exception e) { + Log.w(TAG, "No local address: " + e.getMessage()); + } + return null; + } + private void createQuadCells() { quadCells = new QuadCell[CAM_COUNT]; for (int i = 0; i < CAM_COUNT; i++) { diff --git a/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java b/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java index 073e7bf0..8264b24d 100644 --- a/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java +++ b/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java @@ -8,6 +8,7 @@ package com.openipc.decoder; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -45,4 +46,13 @@ public void emptyOrMissingUpperSlotsDoNotCount() { h[6] = null; assertEquals(2, Decoder.quadGridSize(h, DEF)); } + + @Test + public void udpCellHintNamesPortAndAddress() { + String t = Decoder.udpHintText("192.168.1.20", 5600); + assertTrue(t.contains("UDP RTP")); + assertTrue(t.contains("192.168.1.20:5600")); + // Without a known address the hint still tells where to send the stream + assertTrue(Decoder.udpHintText(null, 5600).contains("this device:5600")); + } } From 4c9a3bb987acb2086481df3085ad9972e5280a42 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 19:12:09 +0000 Subject: [PATCH 2/3] feat: U in the menu shows the reserved UDP RTP cell full-screen (#68) - Menu row: Q, 1-8, U. U stops the current slot or grid and shows the UDP cell full-screen with the same hint (port 5600, this device's address); it is highlighted while shown, and Settings/WebUI are hidden as in multi-view - A slot number or Q leaves the UDP view and starts that slot or the grid (a slot restarts even if it was the active one); a tap on the view opens the menu - The view is not a saved mode: pause/resume and a shared link return to the slot - References point to #68 (pushed RTP over UDP), where the receiver is developed --- README.md | 3 +- README.ru.md | 3 +- README.zh-CN.md | 3 +- .../java/com/openipc/decoder/Decoder.java | 62 ++++++++++++++++++- .../java/com/openipc/decoder/MenuManager.java | 50 +++++++++++---- 5 files changed, 104 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 227c705d..bb6c6e7d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Q** | Turn multi-view (Quad) on or off | | **1**–**8** | Switch to a camera slot. The active slot's number shows the connection quality: green under 100 ms of jitter, yellow 100–300 ms, red above 300 ms. A grey number is an empty slot. | +| **U** | Show the reserved UDP RTP cell full-screen ([#68](https://github.com/OpenIPC/decoder/issues/68)) | | **Settings** | Edit the RTSP URL of the active slot (single-camera mode only) | | **WebUI** | Open the camera's web interface; credentials come from the RTSP URL (single-camera mode only) | | **Exit** | Close the app. The small text next to it is the app version and build hash. | @@ -81,7 +82,7 @@ The app accepts plain-text shares (`ACTION_SEND`, `text/plain`). Sharing an `rts ### Multi-view (Quad) -Shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. The ninth cell of the 3×3 grid is reserved for a camera that pushes RTP over UDP (planned, [#7](https://github.com/OpenIPC/decoder/issues/7)); for now it shows the address and port to send to. Each cell has its own connection and decoder, so the device's video decoder limits how many high-resolution streams play smoothly: with five or more cameras, their substreams are a good choice. Some cameras accept only one RTSP client at a time. Long-press a cell to play its audio (long-press it again to mute); the selected cell shows an AUDIO badge and is remembered. Tap any cell to open the menu. +Shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. The ninth cell of the 3×3 grid is reserved for a camera that pushes RTP over UDP (planned, [#68](https://github.com/OpenIPC/decoder/issues/68)); for now it shows the address and port to send to. **U** in the menu opens it full-screen. Each cell has its own connection and decoder, so the device's video decoder limits how many high-resolution streams play smoothly: with five or more cameras, their substreams are a good choice. Some cameras accept only one RTSP client at a time. Long-press a cell to play its audio (long-press it again to mute); the selected cell shows an AUDIO badge and is remembered. Tap any cell to open the menu. ## Known limitations diff --git a/README.ru.md b/README.ru.md index b358a6d0..73110d4e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -61,6 +61,7 @@ |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Q** | Включить или выключить мультивид (Quad) | | **1**–**8** | Переключиться на слот камеры. Цвет номера активного слота показывает качество связи: зелёный — джиттер меньше 100 мс, жёлтый — 100–300 мс, красный — больше 300 мс. Серый номер — пустой слот. | +| **U** | Показать на весь экран зарезервированную ячейку UDP RTP ([#68](https://github.com/OpenIPC/decoder/issues/68)) | | **Settings** | Изменить RTSP-адрес активного слота (только в режиме одной камеры) | | **WebUI** | Открыть веб-интерфейс камеры; логин и пароль берутся из RTSP-адреса (только в режиме одной камеры) | | **Exit** | Закрыть приложение. Мелкий текст рядом — версия приложения и хеш сборки. | @@ -81,7 +82,7 @@ ### Мультивид (Quad) -Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3 зарезервирована под камеру, которая сама отправляет RTP по UDP (в планах, [#7](https://github.com/OpenIPC/decoder/issues/7)); пока в ней показаны адрес и порт, куда слать поток. У каждой ячейки своё подключение и свой декодер, поэтому число плавно идущих потоков высокого разрешения ограничено видеодекодером устройства: для пяти и более камер лучше брать их дополнительные потоки (substream). Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. +Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3 зарезервирована под камеру, которая сама отправляет RTP по UDP (в планах, [#68](https://github.com/OpenIPC/decoder/issues/68)); пока в ней показаны адрес и порт, куда слать поток. Кнопка **U** в меню открывает её на весь экран. У каждой ячейки своё подключение и свой декодер, поэтому число плавно идущих потоков высокого разрешения ограничено видеодекодером устройства: для пяти и более камер лучше брать их дополнительные потоки (substream). Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. ## Известные ограничения diff --git a/README.zh-CN.md b/README.zh-CN.md index 3aa53a07..df0a52cf 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -61,6 +61,7 @@ |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | **Q** | 开启或关闭多画面模式(Quad) | | **1**–**8** | 切换摄像机槽位。当前槽位编号的颜色表示连接质量:绿色为抖动低于 100 毫秒,黄色为 100–300 毫秒,红色为高于 300 毫秒。灰色编号表示该槽位未配置。 | +| **U** | 全屏显示预留的 UDP RTP 画面([#68](https://github.com/OpenIPC/decoder/issues/68)) | | **Settings** | 编辑当前槽位的 RTSP 地址(仅单路模式) | | **WebUI** | 打开摄像机网页界面,用户名和密码取自 RTSP 地址(仅单路模式) | | **Exit** | 退出应用。旁边的小字是应用版本号和构建哈希。 | @@ -81,7 +82,7 @@ ### 多画面模式(Quad) -以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格预留给通过 UDP 主动推送 RTP 的摄像机(计划中,[#7](https://github.com/OpenIPC/decoder/issues/7));目前其中显示推流目标地址和端口。每个画面都有独立的连接和解码器,因此能流畅播放多少路高分辨率码流取决于设备的视频解码器:五路及以上时建议使用摄像机的子码流。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 +以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格预留给通过 UDP 主动推送 RTP 的摄像机(计划中,[#68](https://github.com/OpenIPC/decoder/issues/68));目前其中显示推流目标地址和端口。菜单中的 **U** 可将其全屏显示。每个画面都有独立的连接和解码器,因此能流畅播放多少路高分辨率码流取决于设备的视频解码器:五路及以上时建议使用摄像机的子码流。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 ## 已知限制 diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index 10121c13..9c41e9b6 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -270,6 +270,12 @@ public void onCodecChanged(boolean h265, AudioConfig audio) { @Override public void onSwitchCamera(int slot) { + if (leaveUdpView()) { + // From the UDP view back to a slot: (re)start it even if it was the active one + mActive = slot; + saveSettings(); + return; + } if (slot == mActive && !quadEnabled) return; if (quadEnabled) { // Exit quad mode and switch to selected camera @@ -284,6 +290,10 @@ public void onSwitchCamera(int slot) { @Override public void onToggleQuad() { + if (leaveUdpView()) { + startQuad(); + return; + } boolean newState = !quadEnabled; if (newState) startQuad(); else { stopQuad(); @@ -296,6 +306,46 @@ public void onToggleQuad() { } } + @Override + public void onShowUdp() { + if (udpViewShown) return; + if (quadEnabled) { + stopQuad(); + } else { + if (rtspClient != null) rtspClient.stop(); + stopExecutor(); + audioManager.closeAudio(); + closeDecoder(); + nalQueue.clear(); + } + mSurface.setVisibility(View.GONE); + if (udpFullView == null) { + udpFullView = udpHintCell(); + udpFullView.setTextSize(20); + // The video view that normally opens the menu on a tap is hidden here + udpFullView.setOnClickListener(v -> menuManager.showMenu(findViewById(R.id.decoder), + quadEnabled, mActive, mVersion, BuildConfig.GIT_HASH, mHosts)); + ((FrameLayout) findViewById(R.id.decoder)).addView(udpFullView, + new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + } else { + udpFullView.setText(udpHintText(localIpv4(), UDP_RTP_PORT)); + } + udpFullView.setVisibility(View.VISIBLE); + udpViewShown = true; + menuManager.setUdpShown(true); + } + + /** Hide the UDP view; true if it was shown. The caller starts what comes next. */ + private boolean leaveUdpView() { + if (!udpViewShown) return false; + udpViewShown = false; + menuManager.setUdpShown(false); + if (udpFullView != null) udpFullView.setVisibility(View.GONE); + mSurface.setVisibility(View.VISIBLE); + return true; + } + @Override public void onExit() { finishAndRemoveTask(); } @@ -572,7 +622,7 @@ private void buildQuadGrid(int size) { int idx = row * size + col; if (idx >= CAM_COUNT) { // The ninth cell of a 3x3 grid is reserved for a pushed UDP RTP stream - // (#7); until the receiver exists it explains what will appear there + // (#68); until the receiver exists it explains what will appear there lr.addView(udpHintCell(), new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, 1)); continue; @@ -610,10 +660,13 @@ private void buildQuadGrid(int size) { } } - /** Port of the reserved UDP RTP cell (#7); 5600 is the usual default for pushed RTP. */ + /** Port of the reserved UDP RTP cell (#68); 5600 is the usual default for pushed RTP. */ static final int UDP_RTP_PORT = 5600; + /** The UDP cell full-screen (menu button U); there is no receiver yet (#68). */ + private TextView udpFullView; + private boolean udpViewShown; - private View udpHintCell() { + private TextView udpHintCell() { TextView hint = new TextView(this); hint.setText(udpHintText(localIpv4(), UDP_RTP_PORT)); hint.setTextColor(Color.GRAY); @@ -992,6 +1045,8 @@ protected void onPause() { super.onPause(); mResumed = false; menuManager.onPauseDismissBrowser(); + // The UDP view is not a saved mode: after a resume the slot or grid comes back + leaveUdpView(); stopStreams(); } @@ -1041,6 +1096,7 @@ protected void onNewIntent(Intent intent) { // Android 10+ may deliver it to the resumed activity without onPause()/onResume(): // apply the shared link now if (mResumed) { + leaveUdpView(); stopStreams(); startStreams(); } diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index c878d263..bada8f66 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -47,6 +47,9 @@ class MenuManager { private LinearLayout mMenuLayout; private TextView[] mCachedCamButtons; private TextView mCachedQuadBtn; + private TextView mCachedUdpBtn; + /** The UDP RTP view is shown: neither a slot nor multi-view is active. */ + private boolean udpShown; private boolean mMenuIsCached; private TextView mSettingsBtn; private TextView mWebUiBtn; @@ -57,6 +60,8 @@ class MenuManager { interface Listener { void onSwitchCamera(int slot); void onToggleQuad(); + /** U: the reserved UDP RTP cell full-screen (#68). */ + void onShowUdp(); void onExit(); void onShowSettings(); void onSaveSettings(); @@ -85,8 +90,10 @@ void updateQuality(int latency) { } } + void setUdpShown(boolean shown) { udpShown = shown; } + void applyQualityToActive(boolean quadEnabled, int mActive) { - if (camButtons != null && !quadEnabled) { + if (camButtons != null && !quadEnabled && !udpShown) { camButtons[mActive].setTextColor(mActiveQualityColor); } } @@ -102,6 +109,7 @@ void dismissPopup() { void showMenu(View anchor, boolean quadEnabled, int mActive, String version, String gitHash, String[] mHosts) { + boolean single = !quadEnabled && !udpShown; if (mMenuIsCached) { updateCachedMenu(quadEnabled, mActive, mHosts); if (mMenuPopup != null) return; @@ -153,7 +161,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { mSettingsBtn = createItem("Settings"); layout.addView(mSettingsBtn); - mSettingsBtn.setVisibility(quadEnabled ? View.GONE : View.VISIBLE); + mSettingsBtn.setVisibility(single ? View.VISIBLE : View.GONE); final TextView quadBtn = createItem("Q"); quadBtn.setGravity(Gravity.CENTER); @@ -161,7 +169,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { mWebUiBtn = createItem("WebUI"); layout.addView(mWebUiBtn); - mWebUiBtn.setVisibility(quadEnabled ? View.GONE : View.VISIBLE); + mWebUiBtn.setVisibility(single ? View.VISIBLE : View.GONE); camButtons = new TextView[CAM_COUNT]; mCachedCamButtons = new TextView[CAM_COUNT]; @@ -175,11 +183,11 @@ public boolean dispatchKeyEvent(KeyEvent event) { LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - if (i == mActive && !quadEnabled) { + if (i == mActive && single) { highlightItem(camButtons[i]); - applyQualityColor(camButtons[i], i, quadEnabled, mActive, mHosts); + applyQualityColor(camButtons[i], i, !single, mActive, mHosts); } else { - applyQualityColor(camButtons[i], i, quadEnabled, mActive, mHosts); + applyQualityColor(camButtons[i], i, !single, mActive, mHosts); } camButtons[i].setOnClickListener(v -> { @@ -200,6 +208,21 @@ public boolean dispatchKeyEvent(KeyEvent event) { if (listener != null) listener.onToggleQuad(); }); + // U after the slots: the reserved UDP RTP cell of the 3x3 grid, full-screen (#68) + final TextView udpBtn = createItem("U"); + udpBtn.setGravity(Gravity.CENTER); + udpBtn.setPadding(dp(12), dp(8), dp(12), dp(8)); + camRow.addView(udpBtn, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + if (udpShown) highlightItem(udpBtn); + mCachedUdpBtn = udpBtn; + udpBtn.setOnClickListener(v -> { + PopupWindow p = mMenuPopup; + if (p != null) p.dismiss(); + if (listener != null) listener.onShowUdp(); + }); + String code = "Exit [v" + version + ", " + gitHash + "]"; SpannableString s = new SpannableString(code); s.setSpan(new SuperscriptSpan(), 5, s.length(), 0); @@ -234,25 +257,30 @@ public boolean dispatchKeyEvent(KeyEvent event) { } private void updateCachedMenu(boolean quadEnabled, int mActive, String[] mHosts) { + boolean single = !quadEnabled && !udpShown; if (mSettingsBtn != null) { - mSettingsBtn.setVisibility(quadEnabled ? View.GONE : View.VISIBLE); + mSettingsBtn.setVisibility(single ? View.VISIBLE : View.GONE); } if (mWebUiBtn != null) { - mWebUiBtn.setVisibility(quadEnabled ? View.GONE : View.VISIBLE); + mWebUiBtn.setVisibility(single ? View.VISIBLE : View.GONE); } if (mCachedQuadBtn != null) { if (quadEnabled) highlightItem(mCachedQuadBtn); else resetItem(mCachedQuadBtn); } + if (mCachedUdpBtn != null) { + if (udpShown) highlightItem(mCachedUdpBtn); + else resetItem(mCachedUdpBtn); + } if (mCachedCamButtons != null && camButtons != null) { for (int i = 0; i < CAM_COUNT; i++) { if (mCachedCamButtons[i] == null || camButtons[i] == null) continue; - if (i == mActive && !quadEnabled) { + if (i == mActive && single) { highlightItem(camButtons[i]); - applyQualityColor(camButtons[i], i, quadEnabled, mActive, mHosts); + applyQualityColor(camButtons[i], i, !single, mActive, mHosts); } else { resetItem(camButtons[i]); - applyQualityColor(camButtons[i], i, quadEnabled, mActive, mHosts); + applyQualityColor(camButtons[i], i, !single, mActive, mHosts); } } } From b1ba131be048a2caa3b03a11b817f9da9f6d565b Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 19:32:19 +0000 Subject: [PATCH 3/3] fix: the ninth grid cell opens the menu on a tap; the UDP view stays on top (Qodo) - udpHintCell() installs the menu-opening click listener, so the grid cell behaves like the camera cells and the full-screen view keeps it - bringToFront() each time U is shown: after U -> an unconfigured slot, that slot's black overlay was in front and would have covered the hint on the next U --- .../app/src/main/java/com/openipc/decoder/Decoder.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index 9c41e9b6..5b59f401 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -322,15 +322,15 @@ public void onShowUdp() { if (udpFullView == null) { udpFullView = udpHintCell(); udpFullView.setTextSize(20); - // The video view that normally opens the menu on a tap is hidden here - udpFullView.setOnClickListener(v -> menuManager.showMenu(findViewById(R.id.decoder), - quadEnabled, mActive, mVersion, BuildConfig.GIT_HASH, mHosts)); ((FrameLayout) findViewById(R.id.decoder)).addView(udpFullView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); } else { udpFullView.setText(udpHintText(localIpv4(), UDP_RTP_PORT)); } + // An unconfigured slot brings its black overlay to the front (clearVideo()): + // the hint must be above it each time it is shown + udpFullView.bringToFront(); udpFullView.setVisibility(View.VISIBLE); udpViewShown = true; menuManager.setUdpShown(true); @@ -674,6 +674,9 @@ private TextView udpHintCell() { hint.setGravity(Gravity.CENTER); hint.setPadding(dp(12), dp(12), dp(12), dp(12)); hint.setBackgroundColor(Color.BLACK); + // Like the camera cells and the video view: a tap opens the menu + hint.setOnClickListener(v -> menuManager.showMenu(findViewById(R.id.decoder), + quadEnabled, mActive, mVersion, BuildConfig.GIT_HASH, mHosts)); return hint; }