From 09feda6a17b067e191124a446fde82cc3cc79f46 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 10:29:11 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20show=20the=20transport=20actually?= =?UTF-8?q?=20in=20use=20=E2=80=94=20single-view=20status,=20debug=20page,?= =?UTF-8?q?=20stats.json=20(#115,=20#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ README.ru.md | 2 ++ README.zh-CN.md | 2 ++ .../java/com/openipc/decoder/Decoder.java | 16 ++++++++++++--- .../java/com/openipc/decoder/QuadCell.java | 3 +++ .../java/com/openipc/decoder/RtspClient.java | 2 ++ .../java/com/openipc/decoder/RtspSession.java | 20 +++++++++++++++++-- .../java/com/openipc/decoder/Transport.java | 12 +++++++++++ .../com/openipc/decoder/TransportTest.java | 12 +++++++++++ 9 files changed, 66 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a68879db..ed0ea06b 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Each slot has its own transport, chosen under the URL in **Settings** (or with t | **Auto** | UDP, or TCP when UDP does not work | Falls back to TCP when the camera refuses UDP or no video arrives within 3 s, then stays on TCP until the slot is changed | | **Multicast** | RTSP multicast (Android 8.0+), or TCP when it is refused | For servers that offer RTSP multicast. OpenIPC Majestic and XM cameras do not: they reply 400 or 451, and the slot plays over TCP in the same connection | +The transport actually in use is shown when a stream starts in single view (`Connected • UDP • H.265 …`), on the debug page and in `/stats.json` (`transport`; the slot's setting is `transport_mode`). After a fallback it reads, for example, `TCP (auto)`. + ### UDP slots (Majestic outgoing) A slot can also receive RTP that the camera pushes by itself, without RTSP. Put a `udp://` URL into the slot instead of `rtsp://`: diff --git a/README.ru.md b/README.ru.md index a1db6a35..01b3844a 100644 --- a/README.ru.md +++ b/README.ru.md @@ -102,6 +102,8 @@ adb shell am start -a android.intent.action.SEND -t text/plain \ | **Auto** | UDP, а если UDP не работает — TCP | Переходит на TCP, если камера отказала в UDP или видео не пришло за 3 с, и остаётся на TCP, пока слот не изменят | | **Multicast** | RTSP multicast (Android 8.0+), при отказе — TCP | Для серверов с RTSP multicast. OpenIPC Majestic и камеры XM его не поддерживают: отвечают 400 или 451, и слот играет по TCP в том же соединении | +Фактический транспорт показывается при запуске потока в одиночном режиме (`Connected • UDP • H.265 …`), на отладочной странице и в `/stats.json` (`transport`; настройка слота — `transport_mode`). После отката там будет, например, `TCP (auto)`. + ### Слоты UDP (Majestic outgoing) Слот может принимать и RTP, который камера отправляет сама, без RTSP. Для этого вместо `rtsp://` укажите адрес `udp://`: diff --git a/README.zh-CN.md b/README.zh-CN.md index 1e4919cb..2112e6d4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -102,6 +102,8 @@ adb shell am start -a android.intent.action.SEND -t text/plain \ | **Auto** | 优先 UDP,UDP 不可用时改用 TCP | 摄像机拒绝 UDP 或 3 秒内未收到视频时回退到 TCP,并保持 TCP 直到修改该槽位 | | **Multicast** | RTSP 组播(Android 8.0+),被拒绝时改用 TCP | 适用于提供 RTSP 组播的服务器。OpenIPC Majestic 和 XM 摄像机不支持:它们返回 400 或 451,槽位会在同一连接中改用 TCP 播放 | +实际使用的传输方式会在单画面模式开始播放时显示(`Connected • UDP • H.265 …`),也会显示在调试页面和 `/stats.json` 中(`transport`;槽位的设置为 `transport_mode`)。回退后会显示为例如 `TCP (auto)`。 + ### UDP 槽位 (Majestic outgoing) 槽位也可以接收摄像机不经 RTSP 主动推送的 RTP。为此请填写 `udp://` 地址代替 `rtsp://`: 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 da3af6f5..5570f8b3 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -575,7 +575,11 @@ private void showConnectedDetails() { / (elapsedNs / 1_000_000_000.0); bps = String.format(Locale.US, " • %.1f Mbps", mbps); } - statusText.setText("Connected • " + videoCodec + aCodec + bps); + // The transport the stream really uses, not the slot's setting (#115) + String inUse = rtspClient != null ? rtspClient.transportInUse() : null; + String transport = inUse != null + ? Transport.label(inUse, mTransports[mActive]) + " • " : ""; + statusText.setText("Connected • " + transport + videoCodec + aCodec + bps); } private void updateStatus(String status) { @@ -1249,7 +1253,11 @@ private Map debugState() { } else if (!quadEnabled) { Map single = new LinkedHashMap<>(); single.put("url", DebugServer.redact(mHost)); - single.put("transport", mTransports[mActive] != null ? mTransports[mActive].name() : null); + // In use (with the mode after a fallback) and the slot's setting (#112) + single.put("transport", Transport.label( + rtspClient != null ? rtspClient.transportInUse() : null, mTransports[mActive])); + single.put("transport_mode", mTransports[mActive] != null + ? mTransports[mActive].name() : null); single.put("video", mResumed && "connected".equals(singleState) && videoDecoder.renderedWithin(SNAPSHOT_LIVE_MS)); single.put("status", statusText != null && statusText.getVisibility() == View.VISIBLE @@ -1276,7 +1284,9 @@ private Map debugState() { TextView v = quadStats[i]; c.put("text", v != null ? v.getText().toString() : null); c.put("url", u ? UDP_CELL_URL : DebugServer.redact(mHosts[i])); - c.put("transport", u ? "UDP" : mTransports[i] != null + // In use (with the mode after a fallback) and the slot's setting (#112) + c.put("transport", cell.transportLabel()); + c.put("transport_mode", u ? "UDP" : mTransports[i] != null ? mTransports[i].name() : null); c.put("out_fps", cell.outFps()); c.put("in_fps", cell.inFps()); diff --git a/android/app/src/main/java/com/openipc/decoder/QuadCell.java b/android/app/src/main/java/com/openipc/decoder/QuadCell.java index 697a7e3b..7dda393c 100644 --- a/android/app/src/main/java/com/openipc/decoder/QuadCell.java +++ b/android/app/src/main/java/com/openipc/decoder/QuadCell.java @@ -333,6 +333,9 @@ static boolean isKeyFrameNal(int type, boolean h265) { return h265 ? type >= 16 && type <= 21 : type == 5; } + /** Transport in use, with the mode after a fallback (#112). */ + String transportLabel() { return Transport.label(session.transportInUse(), transport); } + float inFps() { return inFps; } float outFps() { return outFps; } float lostFps() { return lostFps; } diff --git a/android/app/src/main/java/com/openipc/decoder/RtspClient.java b/android/app/src/main/java/com/openipc/decoder/RtspClient.java index c0cbc69c..bab08e85 100644 --- a/android/app/src/main/java/com/openipc/decoder/RtspClient.java +++ b/android/app/src/main/java/com/openipc/decoder/RtspClient.java @@ -69,6 +69,8 @@ void configure(String host, String userAgent, Transport.Mode transport) { } boolean isActive() { return session.isActive(); } + /** Transport the stream really uses now, or null (#115). */ + String transportInUse() { return session.transportInUse(); } long getStreamBytes() { return session.getStreamBytes(); } long getBitrateMarkNs() { return session.getBitrateMarkNs(); } diff --git a/android/app/src/main/java/com/openipc/decoder/RtspSession.java b/android/app/src/main/java/com/openipc/decoder/RtspSession.java index e35c6424..31f3cda2 100644 --- a/android/app/src/main/java/com/openipc/decoder/RtspSession.java +++ b/android/app/src/main/java/com/openipc/decoder/RtspSession.java @@ -149,9 +149,19 @@ void start() { } } + /** + * Transport of the stream that plays now, as negotiated (#112, #115): "TCP", "UDP", + * "multicast" for RTSP; "UDP push" / "multicast push" for udp:// URLs; null when not + * playing. Unlike the mode of the slot, it shows a fallback (Auto → TCP). + */ + private volatile String inUse; + + String transportInUse() { return inUse; } + /** Stop without waiting: threads exit on their own and deliver no more callbacks. */ void stop() { synchronized (lock) { + inUse = null; generation++; active = false; keepalive = null; @@ -407,8 +417,9 @@ private boolean connect(int gen) throws IOException { active = true; keepalive = new Keepalive(c, u.requestUrl, session, keepaliveMs); udpPlaying = rx != null; + inUse = rx == null ? "TCP" : mc ? "multicast" : "UDP"; } - Log.i(TAG, tag + ": playing over " + (rx == null ? "TCP" : mc ? "multicast" : "UDP")); + Log.i(TAG, tag + ": playing over " + inUse); status(gen, "buffering"); s.setSoTimeout(0); // the watchdog handles silence if (rx != null) { @@ -419,7 +430,10 @@ private boolean connect(int gen) throws IOException { return true; } finally { synchronized (lock) { - if (socket == s) socket = null; + if (socket == s) { + socket = null; + inUse = null; + } // A newer connection may own these by now if (udp == rx) { udp = null; @@ -458,6 +472,7 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException { } listener = l; listening = true; + inUse = u.multicast() ? "multicast push" : "UDP push"; // No RTSP here: nothing of an older connection may be sent by the watchdog keepalive = null; lastPacketMs = System.nanoTime() / 1_000_000; @@ -512,6 +527,7 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException { if (listener == l) { listener = null; listening = false; + inUse = null; } } l.close(); diff --git a/android/app/src/main/java/com/openipc/decoder/Transport.java b/android/app/src/main/java/com/openipc/decoder/Transport.java index 64f44ef0..4eb82b72 100644 --- a/android/app/src/main/java/com/openipc/decoder/Transport.java +++ b/android/app/src/main/java/com/openipc/decoder/Transport.java @@ -34,6 +34,18 @@ static Mode parse(String s) { } } + /** + * What to show for a stream (#112, #115): the transport in use, with the mode when it fell + * back ("TCP (auto)", "TCP (multicast)"); the mode alone while nothing plays yet. + */ + static String label(String inUse, Mode mode) { + String m = mode != null ? mode.name() : "TCP"; + if (inUse == null) return m; + boolean fellBack = "TCP".equals(inUse) + && (mode == Mode.AUTO || mode == Mode.MULTICAST); + return fellBack ? inUse + " (" + m.toLowerCase(java.util.Locale.ROOT) + ")" : inUse; + } + /** RTP over the RTSP connection ("RTP/AVP/TCP" or an interleaved= parameter). */ boolean tcp; boolean multicast; diff --git a/android/app/src/test/java/com/openipc/decoder/TransportTest.java b/android/app/src/test/java/com/openipc/decoder/TransportTest.java index 7671a5dc..f577f28b 100644 --- a/android/app/src/test/java/com/openipc/decoder/TransportTest.java +++ b/android/app/src/test/java/com/openipc/decoder/TransportTest.java @@ -111,4 +111,16 @@ public void usableUdpReplies() { assertFalse(RtspSession.udpReplyUsable(Transport.parse( "RTP/AVP;multicast;destination=239.1.2.3;port=5000-5001"), 50000)); } + + @Test + public void labelShowsTheTransportInUse() { + // Nothing plays yet: the slot's mode + assertEquals("AUTO", Transport.label(null, Transport.Mode.AUTO)); + assertEquals("UDP", Transport.label("UDP", Transport.Mode.AUTO)); + assertEquals("TCP (auto)", Transport.label("TCP", Transport.Mode.AUTO)); + assertEquals("TCP (multicast)", Transport.label("TCP", Transport.Mode.MULTICAST)); + assertEquals("multicast", Transport.label("multicast", Transport.Mode.MULTICAST)); + assertEquals("TCP", Transport.label("TCP", Transport.Mode.TCP)); + assertEquals("UDP push", Transport.label("UDP push", Transport.Mode.UDP)); + } } From 651b1299a36853ab9aaad308c5f23e1521574f26 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 10:46:56 +0000 Subject: [PATCH 2/2] fix: a udp:// slot is "UDP push" only while packets arrive (Qodo on #117) --- .../app/src/main/java/com/openipc/decoder/RtspSession.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/openipc/decoder/RtspSession.java b/android/app/src/main/java/com/openipc/decoder/RtspSession.java index 31f3cda2..1744470c 100644 --- a/android/app/src/main/java/com/openipc/decoder/RtspSession.java +++ b/android/app/src/main/java/com/openipc/decoder/RtspSession.java @@ -472,7 +472,6 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException { } listener = l; listening = true; - inUse = u.multicast() ? "multicast push" : "UDP push"; // No RTSP here: nothing of an older connection may be sent by the watchdog keepalive = null; lastPacketMs = System.nanoTime() / 1_000_000; @@ -492,6 +491,7 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException { // packets), and report "connected" again with the next usable one if (!first[0] && System.nanoTime() / 1_000_000 - lastPacketMs > watchdogMs) { first[0] = true; + inUse = null; // listening, but nothing is being pushed (Qodo on #117) status(gen, "disconnected"); } return true; @@ -517,6 +517,8 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException { lastPacketMs = System.nanoTime() / 1_000_000; if (first[0]) { first[0] = false; + // In use only once something arrives: a quiet port is not a stream + inUse = u.multicast() ? "multicast push" : "UDP push"; status(gen, "connected"); } }