Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/en/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ Unix socket listen paths must be absolute and must not contain whitespace. At st
- Format: `host:port` or `host` (default port: 3478)
- Example: `stun.miwifi.com` or `stun.miwifi.com:3478`

- `--rtsp-nat-mode <auto|stun|zte>` - RTSP NAT traversal mode
- `auto`: Use STUN when `rtsp-stun-server` is configured; otherwise disable NAT traversal (default)
- `stun`: Use `rtsp-stun-server` to discover the public UDP port; a valid STUN server is required
- `zte`: Use the ZTE `ZXV10STB` UDP punch protocol without contacting the configured STUN server
- ZTE mode supports IPv4 only. Its address comes from the actual local endpoint of the connected RTSP TCP socket, so it follows the priority of `r2h-ifname`, `upstream-interface-rtsp`, `upstream-interface`, and the system routing table

### Other Options

- `-S, --video-snapshot` - Enable video snapshot feature (default: disabled)
Expand Down Expand Up @@ -245,6 +251,10 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0
# Configure this when an upstream RTSP server requires a specific User-Agent for compatibility
rtsp-user-agent = rtp2httpd/custom

# RTSP NAT traversal mode: auto, stun, or zte (default: auto)
# Auto uses STUN when rtsp-stun-server is configured; otherwise NAT traversal is disabled
rtsp-nat-mode = auto

# STUN server for RTSP NAT traversal (default: disabled)
# When an RTSP server only supports UDP transport and the client is behind NAT, STUN can be used to attempt NAT traversal (not guaranteed to succeed)
# Format: host:port or host (default port: 3478)
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白
- 格式:`host:port` 或 `host`(默认端口 3478)
- 示例:`stun.miwifi.com` 或 `stun.miwifi.com:3478`

- `--rtsp-nat-mode <auto|stun|zte>` - RTSP NAT 穿透模式
- `auto`:配置了 `rtsp-stun-server` 时使用 STUN,否则不启用 NAT 穿透(默认)
- `stun`:使用 `rtsp-stun-server` 探测公网 UDP 端口;必须配置有效的 STUN 服务器
- `zte`:使用中兴 `ZXV10STB` UDP 打洞协议,不访问已配置的 STUN 服务器
- ZTE 模式仅支持 IPv4。其地址取自 RTSP TCP 连接成功后的实际本地 endpoint,因此会遵循 `r2h-ifname`、`upstream-interface-rtsp`、`upstream-interface` 和系统路由表的优先级

### 其他

- `-S, --video-snapshot` - 启用视频快照功能 (默认: 关闭)
Expand Down Expand Up @@ -243,6 +249,10 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0
# 当上游 RTSP 服务器要求特定 User-Agent 时可配置此项
rtsp-user-agent = rtp2httpd/custom

# RTSP NAT 穿透模式:auto、stun 或 zte(默认: auto)
# auto 在配置了 rtsp-stun-server 时使用 STUN,否则不启用 NAT 穿透
rtsp-nat-mode = auto

# STUN 服务器用于 RTSP NAT 穿透(默认: 禁用)
# 当 RTSP 服务器仅支持 UDP 传输且客户端位于 NAT 后时,可尝试使用 STUN 进行 NAT 穿透(不保证成功)
# 格式: host:port 或 host(默认端口 3478)
Expand Down
2 changes: 2 additions & 0 deletions e2e/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
MockRTSPServerNoTeardownResponse,
MockRTSPServerSilent,
MockRTSPServerUDP,
MockRTSPServerZTE,
)
from .mock_stun import MockSTUNServer
from .ports import (
Expand Down Expand Up @@ -73,6 +74,7 @@
"MockRTSPServerNoTeardownResponse",
"MockRTSPServerSilent",
"MockRTSPServerUDP",
"MockRTSPServerZTE",
"MockSTUNServer",
"MulticastSender",
"R2HProcess",
Expand Down
126 changes: 126 additions & 0 deletions e2e/helpers/mock_rtsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
self._stop = threading.Event()
self.requests_received: list[str] = []
self.requests_detailed: list[dict] = []
self.control_peer: tuple | None = None

# -- lifecycle -----------------------------------------------------------

Expand Down Expand Up @@ -126,6 +127,7 @@ def _accept(self) -> None:

def _handle(self, conn: socket.socket, addr: tuple) -> None:
conn.settimeout(10.0)
self.control_peer = addr
transport_hdr = ""
try:
while True:
Expand Down Expand Up @@ -401,6 +403,130 @@ def _after_play(self, conn: socket.socket, addr: tuple) -> None:
udp_sock.close()


# ---------------------------------------------------------------------------
# MockRTSPServerZTE -- ZTE UDP NAT traversal mode
# ---------------------------------------------------------------------------


class MockRTSPServerZTE(_RTSPServerBase):
"""RTSP server that starts UDP media only after a valid ZTE punch packet."""

def __init__(self, port: int = 0, num_packets: int = 200):
super().__init__(port)
self._num_packets = num_packets
self._server_rtp_socket: socket.socket | None = None
self._server_rtcp_socket: socket.socket | None = None
self._receiver_thread: threading.Thread | None = None
self._play_started = threading.Event()
self._valid_probe = threading.Event()
self._client_rtp_port = 0
self._client_address = ""
self._server_rtp_port = 0
self._server_rtcp_port = 0
self.udp_datagrams: list[tuple[bytes, tuple]] = []

@property
def valid_probe_received(self) -> bool:
return self._valid_probe.is_set()

def stop(self) -> None:
self._play_started.set()
super().stop()
if self._server_rtp_socket:
self._server_rtp_socket.close()
if self._server_rtcp_socket:
self._server_rtcp_socket.close()
if self._receiver_thread:
self._receiver_thread.join(timeout=2)

def _setup_response(self, cseq: str, transport_hdr: str) -> str:
for part in transport_hdr.split(";"):
part = part.strip()
if part.startswith("client_port="):
self._client_rtp_port = int(part.split("=", 1)[1].split("-", 1)[0])
elif part.startswith("client_address="):
self._client_address = part.split("=", 1)[1]

while True:
rtp_port, rtcp_port = find_free_udp_port_pair()
rtp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rtcp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
rtp_socket.bind((self.host, rtp_port))
rtcp_socket.bind((self.host, rtcp_port))
break
except OSError:
rtp_socket.close()
rtcp_socket.close()

self._server_rtp_socket = rtp_socket
self._server_rtcp_socket = rtcp_socket
self._server_rtp_port = rtp_port
self._server_rtcp_port = rtcp_port
self._receiver_thread = threading.Thread(target=self._receive_probes, daemon=True)
self._receiver_thread.start()

return (
"RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
"Transport: MP2T/RTP/UDP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
"Session: t1\r\n\r\n"
% (cseq, self._client_rtp_port, self._client_rtp_port + 1, self._server_rtp_port, self._server_rtcp_port)
)

def _receive_probes(self) -> None:
assert self._server_rtp_socket is not None
self._server_rtp_socket.settimeout(0.05)
while not self._stop.is_set():
try:
payload, source = self._server_rtp_socket.recvfrom(2048)
self.udp_datagrams.append((payload, source))
if self._probe_is_valid(payload, source):
self._valid_probe.set()
except socket.timeout:
if self._play_started.is_set():
return
except OSError:
return

def _probe_is_valid(self, payload: bytes, source: tuple) -> bool:
if not self.control_peer or len(payload) != 84:
return False
expected_ip = socket.inet_aton(self.control_peer[0])
expected_tcp_port = self.control_peer[1]
return (
payload[:8] == b"ZXV10STB"
and payload[8:12] == b"\x7f\xff\xff\xff"
and payload[12:16] == expected_ip
and struct.unpack("!H", payload[16:18])[0] == self._client_rtp_port
and struct.unpack("!H", payload[18:20])[0] == expected_tcp_port
and payload[20:] == bytes(64)
and source[0] == self.control_peer[0]
and source[1] == self._client_rtp_port
)

def _after_play(self, conn: socket.socket, addr: tuple) -> None:
self._play_started.set()
if not self._valid_probe.wait(timeout=2.0) or not self.udp_datagrams:
return
if self._receiver_thread:
self._receiver_thread.join(timeout=0.2)

assert self._server_rtp_socket is not None
destination = self.udp_datagrams[0][1]
seq = 0
ts = 0
try:
for _ in range(self._num_packets):
if self._stop.is_set():
break
self._server_rtp_socket.sendto(make_rtp_packet(seq, ts), destination)
seq = (seq + 1) & 0xFFFF
ts = (ts + 3600) & 0xFFFFFFFF
time.sleep(0.001)
except OSError:
pass


# ---------------------------------------------------------------------------
# MockRTSPServerSilent -- accepts connection but never responds
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading