diff --git a/README.md b/README.md index 94f93793..a68879db 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern | Item | Action | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **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 cell of the 3×3 grid full-screen | +| **U** | The U cell full-screen: the camera that pushes RTP over UDP to port 5600 | | **D** | Text in the corner of every multi-view cell (resolution, codec, decoded/received fps, key frames, status), or in none | | **Q** | Turn multi-view (Quad) on or off | | **Settings** | Edit the URL and the transport of the active slot (single-camera mode only) | @@ -89,7 +89,7 @@ adb shell am start -a android.intent.action.SEND -t text/plain \ ### 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; for now it only shows the address and port to send to, and **U** in the menu opens it full-screen. To watch such a camera today, put a [`udp://` URL](#udp-slots-majestic-outgoing) into any slot. Each cell has its own connection and decoder, and the device's video decoder has a fixed budget: the AG-216 test box, for example, decodes about two 1080p30 streams at full rate. So the grid adapts: cells start on key frames only, then decode every frame in priority order (the cell whose sound plays, then slot order) while the decoder keeps up. Cells beyond that stay on key frames, with the picture updated once per key frame interval (`key frames` in the corner text, menu **D**). The limit is learnt on each device by measurement. Substreams of the cameras, where available, let more cells play at full rate. A cell over the decoder's instance limit shows `Decoder limit: N × H.265`. 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, the **U** cell, plays a camera that pushes RTP over UDP to port 5600 of the device (`udp://@:5600`, see [udp:// slots](#udp-slots-majestic-outgoing)); until a stream arrives it shows every address of the device to send to. **U** in the menu opens it full-screen. Other ports or multicast groups go into a slot as a `udp://` URL. Each cell has its own connection and decoder, and the device's video decoder has a fixed budget: the AG-216 test box, for example, decodes about two 1080p30 streams at full rate. So the grid adapts: cells start on key frames only, then decode every frame in priority order (the U cell while it receives a stream, the cell whose sound plays, then slot order) while the decoder keeps up. Cells beyond that stay on key frames, with the picture updated once per key frame interval (`key frames` in the corner text, menu **D**). The limit is learnt on each device by measurement. Substreams of the cameras, where available, let more cells play at full rate. A cell over the decoder's instance limit shows `Decoder limit: N × H.265`. 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. ## Transports diff --git a/README.ru.md b/README.ru.md index 203f947e..a1db6a35 100644 --- a/README.ru.md +++ b/README.ru.md @@ -60,7 +60,7 @@ | Пункт | Действие | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **1**–**8** | Переключиться на слот камеры. Цвет номера активного слота показывает качество связи: зелёный — джиттер меньше 100 мс, жёлтый — 100–300 мс, красный — больше 300 мс. Серый номер — пустой слот. | -| **U** | Показать на весь экран зарезервированную ячейку UDP сетки 3×3 | +| **U** | Ячейка U на весь экран: камера, которая сама шлёт RTP по UDP на порт 5600 | | **D** | Текст в углу каждой ячейки мультивида (разрешение, кодек, декодировано/получено кадр/с, ключевые кадры, статус) — или ни в одной | | **Q** | Включить или выключить мультивид (Quad) | | **Settings** | Изменить адрес и транспорт активного слота (только в режиме одной камеры) | @@ -89,7 +89,7 @@ adb shell am start -a android.intent.action.SEND -t text/plain \ ### Мультивид (Quad) -Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3 зарезервирована под камеру, которая сама отправляет RTP по UDP; пока в ней показаны только адрес и порт, куда слать поток, а кнопка **U** в меню открывает её на весь экран. Чтобы смотреть такую камеру уже сейчас, укажите в любом слоте [адрес `udp://`](#слоты-udp-majestic-outgoing). У каждой ячейки своё подключение и свой декодер, а у видеодекодера устройства постоянный бюджет: тестовая приставка AG-216, например, декодирует в полном темпе примерно два потока 1080p30. Поэтому сетка подстраивается: ячейки стартуют на ключевых кадрах, затем по приоритету (ячейка со звуком, потом по номеру слота) переходят на все кадры, пока декодер успевает. Остальные остаются на ключевых кадрах — картинка обновляется раз в интервал между ключевыми кадрами (в тексте в углу — `key frames`, меню **D**). Предел определяется замером на каждом устройстве. Дополнительные потоки камер (substream), если они есть, позволяют показать в полном темпе больше ячеек. Ячейка сверх лимита экземпляров декодера показывает `Decoder limit: N × H.265`. Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. +Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3, ячейка **U**, показывает камеру, которая сама отправляет RTP по UDP на порт 5600 устройства (`udp://@:5600`, см. [слоты udp://](#слоты-udp-majestic-outgoing)); пока поток не пришёл, в ней перечислены все адреса устройства, куда его слать. Кнопка **U** в меню открывает её на весь экран. Другие порты и multicast-группы указываются в слоте адресом `udp://`. У каждой ячейки своё подключение и свой декодер, а у видеодекодера устройства постоянный бюджет: тестовая приставка AG-216, например, декодирует в полном темпе примерно два потока 1080p30. Поэтому сетка подстраивается: ячейки стартуют на ключевых кадрах, затем по приоритету (ячейка U, пока в неё идёт поток, ячейка со звуком, потом по номеру слота) переходят на все кадры, пока декодер успевает. Остальные остаются на ключевых кадрах — картинка обновляется раз в интервал между ключевыми кадрами (в тексте в углу — `key frames`, меню **D**). Предел определяется замером на каждом устройстве. Дополнительные потоки камер (substream), если они есть, позволяют показать в полном темпе больше ячеек. Ячейка сверх лимита экземпляров декодера показывает `Decoder limit: N × H.265`. Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. ## Транспорты diff --git a/README.zh-CN.md b/README.zh-CN.md index ab35518e..1e4919cb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -60,7 +60,7 @@ | 菜单项 | 功能 | |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | **1**–**8** | 切换摄像机槽位。当前槽位编号的颜色表示连接质量:绿色为抖动低于 100 毫秒,黄色为 100–300 毫秒,红色为高于 300 毫秒。灰色编号表示该槽位未配置。 | -| **U** | 全屏显示 3×3 网格中预留的 UDP 画面 | +| **U** | 全屏显示 U 画面:通过 UDP 向 5600 端口推送 RTP 的摄像机 | | **D** | 在每个多画面格子的角落显示文字(分辨率、编码、已解码/已接收帧率、关键帧、状态),或全部不显示 | | **Q** | 开启或关闭多画面模式(Quad) | | **Settings** | 编辑当前槽位的地址和传输方式(仅单路模式) | @@ -89,7 +89,7 @@ adb shell am start -a android.intent.action.SEND -t text/plain \ ### 多画面模式(Quad) -以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格预留给通过 UDP 主动推送 RTP 的摄像机;目前其中仅显示推流目标地址和端口,菜单中的 **U** 可将其全屏显示。如需立即观看此类摄像机,请在任意槽位中填写 [`udp://` 地址](#udp-槽位-majestic-outgoing)。每个画面都有独立的连接和解码器,而设备视频解码器的处理能力是固定的:例如测试用的 AG-216 盒子以全帧率大约只能解码两路 1080p30。因此网格会自适应:各画面先只解码关键帧,然后按优先级(正在播放声音的画面,其次按槽位顺序)在解码器跟得上时切换为全帧率。其余画面保持只解码关键帧,画面按关键帧间隔更新(角落文字显示 `key frames`,菜单 **D**)。上限在每台设备上通过实测确定。如摄像机提供子码流,可让更多画面以全帧率播放。超出解码器实例上限的画面显示 `Decoder limit: N × H.265`。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 +以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格即 **U** 画面,播放通过 UDP 向本设备 5600 端口主动推送 RTP 的摄像机(`udp://@:5600`,参见 [udp:// 槽位](#udp-槽位-majestic-outgoing));收到流之前,其中列出本设备可用于推流的所有地址。菜单中的 **U** 可将其全屏显示。其他端口或组播组请在槽位中填写 `udp://` 地址。每个画面都有独立的连接和解码器,而设备视频解码器的处理能力是固定的:例如测试用的 AG-216 盒子以全帧率大约只能解码两路 1080p30。因此网格会自适应:各画面先只解码关键帧,然后按优先级(正在接收流的 U 画面,其次是正在播放声音的画面,再按槽位顺序)在解码器跟得上时切换为全帧率。其余画面保持只解码关键帧,画面按关键帧间隔更新(角落文字显示 `key frames`,菜单 **D**)。上限在每台设备上通过实测确定。如摄像机提供子码流,可让更多画面以全帧率播放。超出解码器实例上限的画面显示 `Decoder limit: N × H.265`。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 ## 传输方式 diff --git a/android/app/src/main/java/com/openipc/decoder/DebugServer.java b/android/app/src/main/java/com/openipc/decoder/DebugServer.java index 1a89e57b..80079536 100644 --- a/android/app/src/main/java/com/openipc/decoder/DebugServer.java +++ b/android/app/src/main/java/com/openipc/decoder/DebugServer.java @@ -508,11 +508,11 @@ private static void closeQuietly(Socket s) { + "$('err').textContent=ui?'UI '+s.ui_thread:none?(s.mode||'no data'):'';\n" + "$('dot').className='dot '+(ui||none?'warn':'ok');\n" + "var list,side=1;\n" - + "if(s.mode==='paused'||s.mode==='udp'||!(s.cells||s.single))list=[s.mode==='udp'?{s" - + "lot:'U',text:'UDP RTP port '+(s.udp?s.udp.port:'')}:{text:s.mode==='paused'?'App" - + " in background':(s.mode||'')}];\n" - + "else if(s.cells){side=s.cells.length>4?3:2;list=s.cells.slice();" - + "while(list.length4?3:2;list=s.cells.slice();" + + "while(list.length { - if (!quadEnabled || quadContainer.getWidth() > 0) return; + if (!(quadEnabled || udpViewShown) || quadContainer.getWidth() > 0) return; Log.w(TAG, "Quad layout did not happen, forcing a layout pass"); View decor = getWindow().getDecorView(); for (View v = quadContainer; v != null && v != decor; @@ -676,7 +687,10 @@ static int quadGridSize(String[] hosts, String defaultUrl) { return 2; } - /** (Re)build the multi-view grid; cells beyond it have no view. */ + /** + * (Re)build the multi-view grid; cells beyond it have no view. Side 3: the ninth cell is + * the U cell; side 1: the U cell alone (the U view). + */ private void buildQuadGrid(int size) { quadContainer.removeAllViews(); Arrays.fill(quadViews, null); @@ -688,14 +702,7 @@ private void buildQuadGrid(int size) { quadContainer.addView(lr, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1)); for (int col = 0; col < size; col++) { - int idx = row * size + col; - if (idx >= CAM_COUNT) { - // The ninth cell of a 3x3 grid is reserved for a pushed UDP RTP stream - // (#68); until the receiver exists it explains what will appear there - lr.addView(udpHintCell(), new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.MATCH_PARENT, 1)); - continue; - } + int idx = size == 1 ? U_CELL : row * size + col; quadViews[idx] = new SurfaceView(this); quadViews[idx].setOnClickListener(cv -> menuManager.showMenu(findViewById(R.id.decoder), quadEnabled, mActive, mVersion, @@ -705,11 +712,13 @@ private void buildQuadGrid(int size) { cell.addView(quadViews[idx], new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); - // Long press selects the cell whose audio plays (#12) - quadViews[idx].setOnLongClickListener(v -> { - toggleQuadAudio(idx); - return true; - }); + // Long press selects the cell whose audio plays (#12); the U cell has video only + if (idx != U_CELL) { + quadViews[idx].setOnLongClickListener(v -> { + toggleQuadAudio(idx); + return true; + }); + } TextView badge = new TextView(this); badge.setText(R.string.quad_audio_badge); badge.setTextColor(Color.WHITE); @@ -741,15 +750,17 @@ private void buildQuadGrid(int size) { } } - /** Port of the reserved UDP RTP cell (#68); 5600 is the usual default for pushed RTP. */ + /** Port of the U cell (#68, #87); 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; + /** What the U cell listens to: any sender, unicast, on UDP_RTP_PORT (#87). */ + static final String UDP_CELL_URL = "udp://@:" + UDP_RTP_PORT; + /** The U cell full-screen (menu button U). */ private boolean udpViewShown; + /** Shown over the U cell until its first frame: where to send the stream. */ private TextView udpHintCell() { TextView hint = new TextView(this); - hint.setText(udpHintText(localIpv4(), UDP_RTP_PORT)); + hint.setText(udpHintText(NetInfo.addresses(), UDP_RTP_PORT)); hint.setTextColor(Color.GRAY); hint.setTextSize(13); hint.setGravity(Gravity.CENTER); @@ -761,36 +772,35 @@ private TextView udpHintCell() { 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; + /** Keeps the addresses in the U cell's hint current (DHCP, cable, Wi-Fi), once a second. */ + private void refreshUdpHint() { + SurfaceView v = quadViews[U_CELL]; + if (v == null) return; + View overlay = ((View) v.getParent()).findViewWithTag("quad_overlay_" + U_CELL); + if (!(overlay instanceof TextView) || overlay.getVisibility() != View.VISIBLE) return; + String text = udpHintText(NetInfo.addresses(), UDP_RTP_PORT); + TextView hint = (TextView) overlay; + if (!text.contentEquals(hint.getText())) hint.setText(text); } - /** 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; + /** Hint of the U cell: every usable IPv4 address of the device, one per line (#87). */ + static String udpHintText(List ips, int port) { + StringBuilder target = new StringBuilder(); + for (String ip : ips) target.append('\n').append(ip).append(':').append(port); + if (target.length() == 0) target.append("\nthis device:").append(port); + return "UDP RTP \u00b7 port " + port + "\nWaiting for a stream\u2026\n\n" + + "Send the camera's RTP/UDP output to" + target; } + private void createQuadCells() { - quadCells = new QuadCell[CAM_COUNT]; - for (int i = 0; i < CAM_COUNT; i++) { + quadCells = new QuadCell[CELL_COUNT]; + for (int i = 0; i < CELL_COUNT; i++) { if (quadViews[i] == null) continue; - String url = mHosts[i]; + String url = i == U_CELL ? UDP_CELL_URL : mHosts[i]; if (url == null || url.isEmpty() || url.equals(DEFAULT_URL)) continue; - quadCells[i] = new QuadCell(i, url, mTransports[i], quadViews[i], framePool, mUserAgent, + Transport.Mode mode = i == U_CELL ? Transport.Mode.UDP : mTransports[i]; + quadCells[i] = new QuadCell(i, url, mode, quadViews[i], framePool, mUserAgent, index -> runOnUiThread(() -> removeQuadOverlay(index))); quadCells[i].start(); } @@ -815,7 +825,7 @@ private void applyQuadAudio() { // A new route: audio of a previously selected cell can no longer reach playback int route = audioManager.newRoute(); QuadCell[] cells = quadCells; - for (int i = 0; i < CAM_COUNT; i++) { + for (int i = 0; i < CELL_COUNT; i++) { QuadCell cell = cells != null ? cells[i] : null; boolean on = i == quadAudio && cell != null; if (cell != null) cell.setAudioSink(on ? audioManager : null, route); @@ -881,7 +891,7 @@ private void removeClearOverlay() { } private void clearQuadViews() { - for (int i = 0; i < CAM_COUNT; i++) showQuadOverlay(i); + for (int i = 0; i < CELL_COUNT; i++) showQuadOverlay(i); } /** Black overlay over a cell; its corner text stays visible above it. */ @@ -891,13 +901,17 @@ private void showQuadOverlay(int i) { FrameLayout cell = (FrameLayout) t.getParent(); View overlay = cell.findViewWithTag("quad_overlay_" + i); if (overlay == null) { - overlay = new View(this); + // The U cell explains where to send the stream until the first frame (#87) + overlay = i == U_CELL ? udpHintCell() : new View(this); + if (i == U_CELL && udpViewShown) ((TextView) overlay).setTextSize(20); overlay.setTag("quad_overlay_" + i); overlay.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); overlay.setBackgroundColor(Color.BLACK); cell.addView(overlay); + } else if (i == U_CELL) { + ((TextView) overlay).setText(udpHintText(NetInfo.addresses(), UDP_RTP_PORT)); } overlay.setVisibility(View.VISIBLE); overlay.bringToFront(); @@ -912,7 +926,7 @@ private void showQuadOverlay(int i) { @Override public void run() { QuadCell[] cells = quadCells; - if (!quadEnabled || cells == null) return; + if (!(quadEnabled || udpViewShown) || cells == null) return; long now = System.nanoTime() / 1_000_000; boolean log = ++ticks % 5 == 0; for (int i = 0; i < cells.length; i++) { @@ -927,6 +941,7 @@ public void run() { } planDecoding(cells, now); updateDebugUrls(); + refreshUdpHint(); quadContainer.postDelayed(this, 1000); } }; @@ -975,12 +990,19 @@ private void planDecoding(QuadCell[] cells, long now) { boolean learntBefore = planner.isLearnt(); java.util.List order = new java.util.ArrayList<>(); - // Priority: the cell whose sound plays, then slot order - if (quadAudio >= 0 && quadAudio < cells.length && cells[quadAudio] != null) { + // Priority: the U cell once its stream plays (#94), the cell whose sound plays, then + // slot order. While U receives nothing it stays last and takes no share + int first = U_CELL < cells.length && cells[U_CELL] != null && cells[U_CELL].isPlaying() + ? U_CELL : -1; + if (first >= 0) order.add(plannerCell(first, cells[first], now)); + if (quadAudio >= 0 && quadAudio < cells.length && cells[quadAudio] != null + && quadAudio != first) { order.add(plannerCell(quadAudio, cells[quadAudio], now)); } for (int i = 0; i < cells.length; i++) { - if (cells[i] != null && i != quadAudio) order.add(plannerCell(i, cells[i], now)); + if (cells[i] != null && i != quadAudio && i != first) { + order.add(plannerCell(i, cells[i], now)); + } } DecodePlanner.Decision d = planner.step(order, now); if (d.action != DecodePlanner.Action.NONE) { @@ -1102,13 +1124,18 @@ private List snapshotCells(int[] side) { List cells = new ArrayList<>(); int width = Snapshots.width(getResources().getDisplayMetrics().widthPixels); side[1] = width; - if (!mResumed || udpViewShown) { + if (!mResumed) { Snapshots.Cell c = new Snapshots.Cell(); - c.label = !mResumed ? "App in background" : "U \u00b7 UDP RTP port " + UDP_RTP_PORT; + c.label = "App in background"; cells.add(c); side[0] = 1; return cells; } + if (udpViewShown) { + cells.add(snapshotCell(U_CELL)); + side[0] = 1; + return cells; + } if (!quadEnabled) { Snapshots.Cell c = new Snapshots.Cell(); c.slot = mActive + 1; @@ -1124,28 +1151,29 @@ private List snapshotCells(int[] side) { return cells; } int size = quadGridSize(mHosts, DEFAULT_URL); + for (int i = 0; i < size * size; i++) cells.add(snapshotCell(i)); + side[0] = size; + return cells; + } + + /** One multi-view cell of a snapshot; slot 0 is the U cell. UI thread only. */ + private Snapshots.Cell snapshotCell(int i) { + Snapshots.Cell c = new Snapshots.Cell(); + c.slot = i == U_CELL ? 0 : i + 1; QuadCell[] quad = quadCells; - for (int i = 0; i < size * size; i++) { - Snapshots.Cell c = new Snapshots.Cell(); - if (i >= CAM_COUNT) { - c.label = "U \u00b7 no receiver yet"; - cells.add(c); - continue; - } - c.slot = i + 1; - QuadCell cell = quad != null ? quad[i] : null; - TextView stats = quadStats[i]; - if (cell == null) { - c.label = "not configured"; - } else { - c.label = stats != null ? stats.getText().toString() : ""; - c.live = cell.showsVideo(SNAPSHOT_LIVE_MS); - c.view = quadViews[i]; + QuadCell cell = quad != null && i < quad.length ? quad[i] : null; + TextView stats = quadStats[i]; + if (cell == null) { + c.label = "not configured"; + } else { + c.label = stats != null ? stats.getText().toString() : ""; + c.live = cell.showsVideo(SNAPSHOT_LIVE_MS); + c.view = quadViews[i]; + if (i == U_CELL && !c.live) { + c.label = "U \u00b7 waiting for RTP on port " + UDP_RTP_PORT; } - cells.add(c); } - side[0] = size; - return cells; + return c; } /** Called on a server thread: the state is read on the UI thread, which owns it. */ @@ -1216,7 +1244,7 @@ private Map debugState() { if (udpViewShown) { Map udp = new LinkedHashMap<>(); udp.put("port", UDP_RTP_PORT); - udp.put("status", "no receiver yet (#87)"); + udp.put("addresses", NetInfo.addresses()); m.put("udp", udp); } else if (!quadEnabled) { Map single = new LinkedHashMap<>(); @@ -1232,12 +1260,13 @@ private Map debugState() { m.put("single", single); } QuadCell[] cells = quadCells; - if (quadEnabled && cells != null) { + if ((quadEnabled || udpViewShown) && cells != null) { List list = new ArrayList<>(); - for (int i = 0; i < CAM_COUNT; i++) { + for (int i = 0; i < CELL_COUNT; i++) { if (quadViews[i] == null) continue; Map c = new LinkedHashMap<>(); - c.put("slot", i + 1); + boolean u = i == U_CELL; + c.put("slot", u ? "U" : i + 1); QuadCell cell = cells[i]; if (cell == null) { c.put("video", false); @@ -1246,8 +1275,9 @@ private Map debugState() { c.put("video", cell.showsVideo(SNAPSHOT_LIVE_MS)); TextView v = quadStats[i]; c.put("text", v != null ? v.getText().toString() : null); - c.put("url", DebugServer.redact(mHosts[i])); - c.put("transport", mTransports[i] != null ? mTransports[i].name() : null); + c.put("url", u ? UDP_CELL_URL : DebugServer.redact(mHosts[i])); + c.put("transport", u ? "UDP" : mTransports[i] != null + ? mTransports[i].name() : null); c.put("out_fps", cell.outFps()); c.put("in_fps", cell.inFps()); c.put("lost_fps", cell.lostFps()); 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 8264b24d..ede0a841 100644 --- a/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java +++ b/android/app/src/test/java/com/openipc/decoder/QuadGridTest.java @@ -49,10 +49,29 @@ public void emptyOrMissingUpperSlotsDoNotCount() { @Test public void udpCellHintNamesPortAndAddress() { - String t = Decoder.udpHintText("192.168.1.20", 5600); + String t = Decoder.udpHintText(java.util.Arrays.asList("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")); + assertTrue(Decoder.udpHintText(java.util.Collections.emptyList(), 5600) + .contains("this device:5600")); + } + + @Test + public void theUCellIsTheNinthCell() { + assertEquals(Decoder.CAM_COUNT, Decoder.U_CELL); + assertEquals(9, Decoder.CELL_COUNT); + assertEquals("udp://@:5600", Decoder.UDP_CELL_URL); + } + + @Test + public void udpHintListsEveryAddress() { + String none = Decoder.udpHintText(java.util.Collections.emptyList(), 5600); + assertTrue(none, none.endsWith("\nthis device:5600")); + String one = Decoder.udpHintText(java.util.Arrays.asList("172.17.32.144"), 5600); + assertTrue(one, one.endsWith("output to\n172.17.32.144:5600")); + String two = Decoder.udpHintText( + java.util.Arrays.asList("172.17.32.144", "192.168.1.20"), 5600); + assertTrue(two, two.endsWith("output to\n172.17.32.144:5600\n192.168.1.20:5600")); } }