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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern
```
rtsp://username:password@ip_address:554/stream=0
```
Under the URL you can choose the [transport](#transports); TCP is the default.
Under the URL you can choose the [transport](#transports); Auto (UDP, falling back to TCP) is the default for a new slot.
4. Press **Save**. The app connects straight away and reconnects automatically if the stream drops.

## Controls
Expand Down Expand Up @@ -98,9 +98,9 @@ Each slot has its own transport, chosen under the URL in **Settings** (or with t

| Transport | How the video arrives | Notes |
|-------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| **TCP** (default) | RTP inside the RTSP connection | Works everywhere, also through NAT and firewalls |
| **TCP** | RTP inside the RTSP connection | Works everywhere, also through NAT and firewalls |
| **UDP** | RTP over UDP, a port pair per track | Lower latency on a LAN. Lost packets are skipped, not resent. Never falls back |
| **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 |
| **Auto** (default) | 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)`.
Expand Down
6 changes: 3 additions & 3 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
```
rtsp://username:password@ip_address:554/stream=0
```
Под адресом можно выбрать [транспорт](#транспорты); по умолчанию TCP.
Под адресом можно выбрать [транспорт](#транспорты); у нового слота по умолчанию Auto (UDP с откатом на TCP).
4. Нажмите **Save**. Приложение сразу подключится и само переподключится, если поток прервётся.

## Управление
Expand Down Expand Up @@ -98,9 +98,9 @@ adb shell am start -a android.intent.action.SEND -t text/plain \

| Транспорт | Как приходит видео | Примечание |
|------------------------|-------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| **TCP** (по умолчанию) | RTP внутри RTSP-соединения | Работает везде, в том числе через NAT и межсетевые экраны |
| **TCP** | RTP внутри RTSP-соединения | Работает везде, в том числе через NAT и межсетевые экраны |
| **UDP** | RTP по UDP, пара портов на дорожку | Меньше задержка в локальной сети. Потерянные пакеты пропускаются, а не запрашиваются заново. На TCP не переходит |
| **Auto** | UDP, а если UDP не работает — TCP | Переходит на TCP, если камера отказала в UDP или видео не пришло за 3 с, и остаётся на TCP, пока слот не изменят |
| **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)`.
Expand Down
6 changes: 3 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
```
rtsp://username:password@ip_address:554/stream=0
```
地址下方可选择[传输方式](#传输方式),默认为 TCP。
地址下方可选择[传输方式](#传输方式),新槽位默认为 Auto(优先 UDP,必要时回退到 TCP)。
4. 点击 **Save**。应用会立即连接;视频流中断时会自动重连。

## 操作说明
Expand Down Expand Up @@ -98,9 +98,9 @@ adb shell am start -a android.intent.action.SEND -t text/plain \

| 传输方式 | 视频如何到达 | 说明 |
|-----------------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| **TCP**(默认) | RTP 封装在 RTSP 连接中 | 适用于所有环境,也可穿过 NAT 和防火墙 |
| **TCP** | RTP 封装在 RTSP 连接中 | 适用于所有环境,也可穿过 NAT 和防火墙 |
| **UDP** | 通过 UDP 传输 RTP,每路一对端口 | 局域网内延迟更低。丢失的包直接跳过,不会重传。不会回退到 TCP |
| **Auto** | 优先 UDP,UDP 不可用时改用 TCP | 摄像机拒绝 UDP 或 3 秒内未收到视频时回退到 TCP,并保持 TCP 直到修改该槽位 |
| **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)`。
Expand Down
38 changes: 37 additions & 1 deletion android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ private void loadSettings() {
String url = credentials.open(stored);
mHosts[i] = url != null ? url : DEFAULT_URL;
mUnreadable[i] = url == null && stored != null ? stored : null;
mTransports[i] = Transport.Mode.parse(pref.getString("transport_" + i, null));
// Auto for a slot that has never stored one (#112)
mTransports[i] = Transport.Mode.stored(pref.getString("transport_" + i, null));
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
// Plain URLs from older versions are re-saved encrypted right away
if (credentials.needsMigration(stored)) {
if (migrate == null) migrate = pref.edit();
Expand Down Expand Up @@ -1588,7 +1589,39 @@ protected void onPause() {
stopStreams();
}

/**
* Single view over UDP (#128, like the multi-view cells in #125): packets arrive but no
* frame is decoded for UDP_STALL_SECONDS — a lossy UDP path keeps breaking the key
* frames. With Auto the session then goes over to TCP. Checked once a second.
*/
private static final int UDP_STALL_SECONDS = 8;
private int singleStallSeconds;
private int lastSingleFrames;
private long lastSingleBytes;
private final Runnable singleUdpCheck = new Runnable() {
@Override
public void run() {
if (!mResumed) return;
RtspClient client = rtspClient;
int frames = videoDecoder.framesOut();
long bytes = client != null ? client.getStreamBytes() : 0;
boolean stalled = !quadEnabled && !udpViewShown && client != null
&& client.playsOverUdp() && "connected".equals(singleState)
&& bytes - lastSingleBytes > 10_000 && frames == lastSingleFrames;
lastSingleFrames = frames;
lastSingleBytes = bytes;
singleStallSeconds = stalled ? singleStallSeconds + 1 : 0;
if (singleStallSeconds >= UDP_STALL_SECONDS) {
singleStallSeconds = 0;
client.reportUdpBroken("packets but no decoded frame for " + UDP_STALL_SECONDS
+ " s");
}
mSurface.postDelayed(this, 1000);
}
};

private void stopStreams() {
mSurface.removeCallbacks(singleUdpCheck);
// The single view's stream ends here: it is no longer "connected" for the debug page
singleState = "disconnected";
if (quadCells != null) {
Expand Down Expand Up @@ -1654,6 +1687,9 @@ protected void onResume() {
private boolean mResumed;

private void startStreams() {
singleStallSeconds = 0;
mSurface.removeCallbacks(singleUdpCheck);
mSurface.postDelayed(singleUdpCheck, 1000);
loadSettings();
videoDecoder.setEnabled(true);
audioManager.setEnabled(true);
Expand Down
4 changes: 4 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,10 @@ void configure(String host, String userAgent, Transport.Mode transport) {
}

boolean isActive() { return session.isActive(); }
/** RTP over UDP or multicast at the moment (#128). */
boolean playsOverUdp() { return session.playsOverUdp(); }
/** Packets arrive but nothing decodes: with Auto the session goes over to TCP. */
void reportUdpBroken(String why) { session.reportUdpBroken(why); }
/** Transport the stream really uses now, or null (#115). */
String transportInUse() { return session.transportInUse(); }
long getStreamBytes() { return session.getStreamBytes(); }
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 @@ -331,7 +331,16 @@ private boolean connect(int gen) throws IOException {
}
}

int videoPort = rx != null && !mc ? rx.addTrack(CHANNEL_VIDEO) : -1;
int videoPort = -1;
if (rx != null && !mc) {
try {
videoPort = rx.addTrack(CHANNEL_VIDEO);
} catch (IOException e) {
// No local UDP ports: with Auto the next attempt goes over TCP (Qodo on #128)
if (fallback) markUdpFailed(m);
throw e;
}
}
String transport = rx == null ? Transport.tcpRequest(CHANNEL_VIDEO)
: mc ? Transport.multicastRequest() : Transport.udpRequest(videoPort);
r = exchange(c, "SETUP", sdp.videoUrl, "Transport: " + transport + "\r\n");
Expand Down Expand Up @@ -381,7 +390,12 @@ private boolean connect(int gen) throws IOException {
"Transport: " + transport + "\r\n" +
"Session: " + session + "\r\n");
Transport audioReply = Transport.parse(r.header("transport"));
if (!r.ok()) {
if (!r.ok() && auto && rx != null) {
// Auto: rather TCP for both tracks than UDP video without sound (#128)
markUdpFailed(m);
throw new IOException("audio SETUP over UDP failed (" + r.code
+ "), using TCP");
} else if (!r.ok()) {
// Not fatal: play video without audio
Log.w(TAG, tag + ": audio SETUP failed (" + r.code + "), no audio");
audio = false;
Expand All @@ -394,6 +408,10 @@ private boolean connect(int gen) throws IOException {
}
} else if (rx != null && (audioReply.tcp
|| !udpReplyUsable(audioReply, audioPort))) {
if (auto) {
markUdpFailed(m);
throw new IOException("audio not over UDP like video, using TCP");
}
// Video over UDP, audio elsewhere: the UDP loop cannot play it
Log.w(TAG, tag + ": audio not over UDP like video, no audio");
audio = false;
Expand Down
8 changes: 8 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 @@ -32,6 +32,14 @@ static Mode parse(String s) {
}
return TCP;
}

/**
* The transport of a slot from the settings: Auto when none is stored yet (a new
* install or slot, #112), an unknown value TCP as before.
*/
static Mode stored(String s) {
return s == null ? AUTO : parse(s);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public void modeFromSettings() {
assertEquals(Transport.Mode.TCP, Transport.Mode.parse(null));
assertEquals(Transport.Mode.TCP, Transport.Mode.parse("quic"));
assertEquals(Transport.Mode.MULTICAST, Transport.Mode.parse("multicast"));
// A slot without a stored transport is Auto (#112); stored values stay as they are
assertEquals(Transport.Mode.AUTO, Transport.Mode.stored(null));
assertEquals(Transport.Mode.TCP, Transport.Mode.stored("TCP"));
assertEquals(Transport.Mode.UDP, Transport.Mode.stored("udp"));
}

@Test
Expand Down
Loading