Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://`:
Expand Down
2 changes: 2 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://`:
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://`:
Expand Down
16 changes: 13 additions & 3 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -1249,7 +1253,11 @@ private Map<String, Object> debugState() {
} else if (!quadEnabled) {
Map<String, Object> 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
Expand All @@ -1276,7 +1284,9 @@ private Map<String, Object> 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());
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/QuadCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/RtspClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }

Expand Down
22 changes: 20 additions & 2 deletions android/app/src/main/java/com/openipc/decoder/RtspSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -477,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;
Expand All @@ -502,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");
}
}
Expand All @@ -512,6 +529,7 @@ private boolean listen(int gen, RtpListener.Url u) throws IOException {
if (listener == l) {
listener = null;
listening = false;
inUse = null;
}
}
l.close();
Expand Down
12 changes: 12 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/Transport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions android/app/src/test/java/com/openipc/decoder/TransportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading