Skip to content

feat: RTSP multicast, with fallback to TCP (#69) - #73

Merged
compot-dev merged 3 commits into
v2from
feat/rtsp-multicast
Oct 12, 2026
Merged

compot-dev merged 3 commits into
v2from
feat/rtsp-multicast

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Closes #69. Stacked on #72 (plain RTP over UDP), which is stacked on #70 (UDP for RTSP).

What changes

A fourth transport for a slot, Multicast, next to TCP / UDP / Auto in the URL dialog (--es transport multicast with a shared link):

  • SETUP asks for RTP/AVP;multicast. The reply's destination and port are joined on the network interface of the RTSP connection, and the stream is received by the UDP loop of feat: RTSP over UDP unicast, with fallback to TCP (#67) #70: one thread, reorder buffer, sender check against the RTSP peer and source, keepalives over RTSP, TEARDOWN on close. The Wi‑Fi multicast lock of feat: receive plain RTP from Majestic outgoing over UDP, unicast and multicast (#68) #72 is held while joined.
  • Falls back to TCP, like Auto, in three cases: the SETUP is refused, the reply is not a usable multicast group, or no video arrives within 3 s. The fallback is remembered for the URL (10 minutes). Neither OpenIPC camera supports RTSP multicast, as found in RTSP multicast and other RTSP transports (research: Majestic RTSP has no multicast) #69: Majestic answers 400 'client_port' not found, XM answers 451, and both turn a multicast request that carries client_port into unicast. So with these cameras a Multicast slot simply plays over TCP. The mode is meant for third-party servers and NVRs.
  • A failed multicast is remembered separately from Auto's UDP, so it does not stop Auto from trying UDP on the same URL.
  • Android 8 or later: joining a group from a DatagramChannel needs API 26 on Android (the lint check caught that it is not 24). On older systems a Multicast slot uses TCP.

Tests (RtspSessionTest, scripted server)

  • Majestic-style 400 → second SETUP over TCP in the same connection.
  • A unicast reply to the multicast request → reconnect over TCP.
  • API below 26 → TCP right away.
  • A real multicast reply → group joined on loopback and video received with no TCP SETUP. This test skips itself on a machine where loopback multicast does not work; on the GitHub runner it runs (not skipped).

Device test (when the hardware is free)

  • Majestic / XM with Multicast selected: falls back to TCP at once and plays
  • A server that does RTSP multicast (e.g. live555 / mediamtx with multicast enabled) on the LAN: video received from the group on the AG-216 over Ethernet, and on a Wi‑Fi device

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Bad multicast ports delay fallback ✓ Resolved
Description
joinGroup() checks that reply.port is present but does not validate the RTP port before passing
it to the multicast receiver. When a SETUP reply advertises port=0, the receiver binds an
unrelated ephemeral port; this can leave audio unreachable while video continues playing, because
the fallback timeout checks video only.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R517-522]

+        if (!reply.multicast || group == null || !group.isMulticastAddress()
+                || reply.port == null) {
+            return false;
+        }
+        try {
+            rx.addMulticastTrack(channel, group, reply.port[0], local);
Evidence
Transport.range permits zero, and joinGroup() checks only that reply.port is non-null before
passing its RTP port to the multicast receiver. Binding port zero selects an ephemeral port rather
than the advertised port, yet the audio SETUP path treats the join as successful; the fallback
timeout checks video only, so continuing video can leave the unusable audio join undetected.

Negotiate and validate RTSP multicast transport
Fall back when multicast is unavailable
android/app/src/main/java/com/openipc/decoder/RtspSession.java[515-524]
android/app/src/main/java/com/openipc/decoder/Transport.java[80-91]
android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[143-151]
android/app/src/main/java/com/openipc/decoder/Transport.java[89-99]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[515-528]
android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[143-155]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[361-367]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`joinGroup()` accepts a multicast RTP port of zero, causing the receiver to bind an ephemeral port instead of rejecting an unusable SETUP reply. This can leave audio unreachable while video continues playing and the video-only fallback timeout does not detect the audio failure.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[515-522]
- android/app/src/main/java/com/openipc/decoder/Transport.java[80-91]
- android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[143-155]
## Recommended Fix
Validate that the multicast RTP port is in the valid nonzero transport-port range before attempting either video or audio joins, and return false for invalid values rather than binding a socket. Let the existing multicast failure path retry with TCP where applicable, and ensure an invalid audio reply disables audio rather than treating an ephemeral bind as a successful join. Add regression coverage for a multicast reply containing `port=0-1`, including a zero-port audio reply.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Multicast video from media hosts is lost ✓ Resolved
Description
connect joins the multicast group but still configures UdpReceiver to accept packets only from
the RTSP peer and any addresses named in the SETUP replies. If a server omits source and sends
video from a different media interface, the receiver discards every packet and switches the slot to
TCP after three seconds.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[522]

+            rx.addMulticastTrack(channel, group, reply.port[0], local);
Evidence
The new multicast track uses the same receiver as unicast. The session installs a peer-based
allowlist, and the receive loop applies it to every track before delivering a packet; rejected video
cannot satisfy the first-packet timeout.

android/app/src/main/java/com/openipc/decoder/RtspSession.java[380-383]
android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[199-212]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[614-631]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Multicast RTP from a separate media address is discarded when SETUP does not provide a `source` address.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[380-383]
- android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[166-171]
- android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[199-209]
## Recommended Fix
Keep the existing sender restriction for unicast tracks. For multicast tracks, accept group traffic when no `source` is advertised, and apply the advertised source to the appropriate track when it is present; add a test with a media sender distinct from the RTSP peer.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Two multicast slots from one NVR can mix their video ✓ Resolved
Description
addMulticastTrack binds the group socket to the wildcard address (new InetSocketAddress(port))
with SO_REUSEADDR, not to the group address. On Linux/Android, a socket bound to the wildcard
address also receives datagrams for any other group joined on that port on the device
(IP_MULTICAST_ALL is on by default). The only filter is the sender check (the RTSP peer and
source). So if two slots play channels from the same NVR, on different groups that share a port,
each slot's reorder buffer and decoder also get the other channel's RTP packets.
Code

android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[R146-151]

+            // Other receivers of the group on this device bind the same port
+            ch.setOption(StandardSocketOptions.SO_REUSEADDR, true);
+            ch.bind(new InetSocketAddress(port));
+            ch.setOption(StandardSocketOptions.SO_RCVBUF,
+                    channel == RtspSession.CHANNEL_VIDEO ? VIDEO_RCVBUF : AUDIO_RCVBUF);
+            ch.join(group, nif);
Evidence
The new multicast track binds to the wildcard address on the server-chosen port and joins the group.
run() accepts a datagram for a track when its sender is in sources. setSources in RtspSession
only adds the RTSP peer address, reply.source and the audio source. Packets from the same server
sent to a different group on the same port therefore pass the check and are pushed into this track's
reorder buffer.

android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[137-164]
android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[195-216]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[379-383]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UdpReceiver.addMulticastTrack` binds the DatagramChannel to `0.0.0.0:port`. On Linux/Android, that socket also receives traffic for other groups joined on the same port on the device. Two RTSP multicast slots from the same server, on different groups that share a port, then get each other's packets, because the sender filter only checks the server address.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/UdpReceiver.java[146-151]
## Recommended Fix
Bind to the group address: `ch.bind(new InetSocketAddress(group, port))`. Linux delivers only that group's datagrams to a socket bound this way. Keep SO_REUSEADDR so that other receivers of the same group still work. As an alternative, use a source-specific join (`ch.join(group, nif, source)`) when the reply gives `source`. Add a test in which two receivers join different groups on the same port and each receives only its own traffic.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/RtspSession.java
Comment thread android/app/src/main/java/com/openipc/decoder/RtspSession.java
Comment thread android/app/src/main/java/com/openipc/decoder/UdpReceiver.java Outdated
compot-dev added a commit that referenced this pull request Oct 12, 2026
On Linux a socket bound to the wildcard address receives every group
joined on that port on the device, so two slots on different groups with
the same port would mix their streams (found by the review of #73).
@compot-dev compot-dev added the awaiting device test PR ready, merge only after testing on a real device label Oct 12, 2026
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Code done, CI green, Qodo findings addressed. Waiting for the device round in #74.

A fourth transport for a slot, Multicast: SETUP asks for
"RTP/AVP;multicast", the reply's destination and port are joined on the
interface of the RTSP connection, and the stream is received by the UDP
loop of #67 (TEARDOWN on close, keepalives over RTSP).

It is for third-party servers and NVRs: Majestic answers a multicast
SETUP with 400 and XM cameras with 451, and both turn a multicast request
that carries client_port into unicast. So Multicast falls back to TCP,
like Auto, when the SETUP is refused, when the reply is not a usable
multicast group, or when no video arrives within 3 s, and remembers it
for the URL. A failed multicast does not mark the URL for Auto's UDP.

Joining a group from a DatagramChannel needs API 24: below Android 7 the
slot uses TCP. The Wi-Fi multicast lock of #68 is held while joined.
- A reply with port 0 is not a usable group: fall back instead of binding
  an unrelated port.
- Multicast is not restricted to the RTSP peer: the group selects the
  stream and a server may send it from another interface; only a named
  source restricts it.
- The group socket binds to the group address, not the wildcard, so two
  slots on different groups with one port do not mix their streams.
compot-dev added a commit that referenced this pull request Oct 12, 2026
On Linux a socket bound to the wildcard address receives every group
joined on that port on the device, so two slots on different groups with
the same port would mix their streams (found by the review of #73).
@compot-dev
compot-dev marked this pull request as ready for review October 12, 2026 20:25
@compot-dev compot-dev removed the awaiting device test PR ready, merge only after testing on a real device label Oct 12, 2026
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Device test: .133 → 400 and .53 → 451, both fall back to TCP in the same connection. No RTSP multicast server in the lab. See #74 run 1.

@compot-dev
compot-dev changed the base branch from feat/rtp-udp to v2 October 12, 2026 20:26
@compot-dev
compot-dev merged commit f006b8b into v2 Oct 12, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/rtsp-multicast branch October 13, 2026 17:16
compot-dev added a commit that referenced this pull request Oct 15, 2026
On Linux a socket bound to the wildcard address receives every group
joined on that port on the device, so two slots on different groups with
the same port would mix their streams (found by the review of #73).
compot-dev added a commit that referenced this pull request Oct 15, 2026
feat: RTSP multicast, with fallback to TCP (#69)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RTSP multicast and other RTSP transports (research: Majestic RTSP has no multicast)

1 participant