rushaudio 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: rushaudio
3
+ Version: 1.1.0
4
+ Summary: Low-latency audio over IP live streaming protocol (Python port)
5
+ Author: RushAudio Contributors
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/OseMine/rushaudio
8
+ Keywords: audio,streaming,low-latency,protocol,udp
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Multimedia :: Sound/Audio
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # RushAudio — Python port
23
+
24
+ A pure-Python, zero-dependency implementation of the [RushAudio](https://github.com/OseMine/rushaudio) protocol: a **binary protocol for low-latency audio streaming over UDP**.
25
+
26
+ This port mirrors the public API and wire format of the Rust reference implementation, so it is interoperable byte-for-byte on the wire.
27
+
28
+ | Property | Value |
29
+ |----------|-------|
30
+ | Transport | UDP (default port 4210) |
31
+ | Header size | 14 bytes |
32
+ | Max payload | 4096 bytes |
33
+ | Latency target | 30–100ms |
34
+ | Loss recovery | XOR FEC (recovers 1 loss per group) |
35
+ | Codecs | Opus, PCM S16LE, A-law, μ-law |
36
+ | Metadata | TLV key-value pairs (built-in + custom) |
37
+ | Byte order | Big-endian (except PCM samples: little-endian) |
38
+ | Python | ≥ 3.9, standard library only |
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install rushaudio # from PyPI (once published)
44
+ # or build/install from source:
45
+ cd ports/python
46
+ python -m pip wheel . -w dist # -> dist/rushaudio-1.1.0-py3-none-any.whl
47
+ pip install dist/rushaudio-1.1.0-py3-none-any.whl
48
+ ```
49
+
50
+ ## Quick start
51
+
52
+ ```python
53
+ import rushaudio
54
+ from rushaudio.prelude import *
55
+
56
+ server = rushaudio.create_server("0.0.0.0:4210") # receiver
57
+ client = rushaudio.create_client() # sender
58
+ peer = "127.0.0.1:4210"
59
+
60
+ # Encode/decode an audio frame (AudioData payload, spec §4.1)
61
+ payload = Packet.encode_audio_payload(2, 48000, AudioCodec.OPUS, opus_bytes)
62
+ pkt = Packet.new(PacketType.AUDIO_DATA, seq=1, ts=0, payload=payload)
63
+ client.send_packet(pkt, peer)
64
+
65
+ got = server.recv_packet() # non-blocking: None if nothing available
66
+ if got is not None:
67
+ packet, addr = got
68
+ ch, sr, codec, frame = Packet.decode_audio_payload(packet.payload)
69
+ ```
70
+
71
+ ## Metadata (TLV)
72
+
73
+ ```python
74
+ meta = (
75
+ MetadataBuilder()
76
+ .track_title("My Song")
77
+ .artist("Artist Name")
78
+ .album("Album")
79
+ .sample_rate(48000)
80
+ .channels(2)
81
+ .codec_info("Opus")
82
+ .bitrate(128000)
83
+ .custom(0x80, b"app-specific-data")
84
+ .build()
85
+ )
86
+ client.send_packet(meta.to_packet(seq=2, ts=20), peer)
87
+
88
+ received = Metadata.from_packet(packet) # on the other side
89
+ title = received.get_string(META_TRACK_TITLE) # -> "My Song"
90
+ ```
91
+
92
+ ## Handshake
93
+
94
+ ```python
95
+ config = StreamConfig(sample_rate=48000, channels=2, bitrate=128000, codec=AudioCodec.OPUS)
96
+ hs = Handshake(role=HandshakeRole.INITIATOR, ssrc=12345, config=config)
97
+ client.send_packet(hs.build_request(0, 0), peer)
98
+
99
+ # server side:
100
+ got = server.recv_packet()
101
+ remote_ssrc, cfg = Handshake.parse_request(got[0])
102
+ responder = Handshake(role=HandshakeRole.RESPONDER, ssrc=67890, config=cfg)
103
+ server.send_packet(responder.build_response(0, 0, accepted=True), got[1])
104
+ ```
105
+
106
+ ## FEC recovery
107
+
108
+ ```python
109
+ group = [pkt1, pkt2, pkt3] # original AudioData packets
110
+ repair = FecEncoder.generate_repair(group, seq=100, ts=0)
111
+ client.send_packet(repair, peer)
112
+
113
+ # receiver, having lost pkt2 of the group:
114
+ recovered = FecEncoder.try_recover(repair, [pkt1, pkt3]) # -> (2, b"...")
115
+ ```
116
+
117
+ ## Jitter buffer
118
+
119
+ ```python
120
+ jb = JitterBuffer() # 80ms target delay
121
+ jb.push(seq, ts, payload_bytes) # inserts in sequence order
122
+ out = jb.pop() # (ts, payload) once aged, else None
123
+ jb.adapt_delay() # clamp(2*jitter + 10ms, 20ms, 400ms)
124
+ stats = jb.stats() # depth, dropped, late, jitter
125
+ ```
126
+
127
+ ## Wire / raw bytes
128
+
129
+ ```python
130
+ wire = pkt.encode() # header + payload, 14+N bytes
131
+ parsed = Packet.decode(wire) # raises PacketError subclasses on invalid data
132
+ ```
133
+
134
+ `UdpTransport.recv_packet()` returns `(Packet, (host, port))` or `None` when
135
+ nothing is available (non-blocking). Non-parseable datagrams are skipped.
136
+
137
+ ## Layout
138
+
139
+ ```
140
+ ports/python/
141
+ ├── pyproject.toml
142
+ ├── rushaudio/
143
+ │ ├── __init__.py # public API: create_server / create_client / DEFAULT_PORT
144
+ │ ├── prelude.py # from rushaudio.prelude import *
145
+ │ ├── constants.py # magic, metadata keys, timing, FEC/level constants
146
+ │ ├── types.py # PacketType, AudioCodec, StreamConfig, StreamStats
147
+ │ ├── packet.py # 14-byte header, Packet encode/decode, errors
148
+ │ ├── metadata.py # Metadata / MetadataBuilder TLV codec
149
+ │ ├── levels.py # AudioLevels (VU meter) packet type
150
+ │ ├── fec.py # XOR FEC encode + single-loss recovery
151
+ │ ├── jitter.py # adaptive jitter buffer
152
+ │ ├── handshake.py # handshake state machine + payload builders
153
+ │ ├── transport.py # non-blocking UdpTransport
154
+ │ ├── connection.py # Connection + bounded ConnectionPool
155
+ │ └── session.py # SessionManager (keepalive, stale sweeping)
156
+ └── tests/
157
+ └── test_protocol.py # 37 tests (mirrors tests/integration.rs)
158
+ ```
159
+
160
+ ## Running the tests
161
+
162
+ ```bash
163
+ cd ports/python
164
+ python -m unittest discover -s tests -v # 37 tests, stdlib only
165
+ ```
166
+
167
+ ## Parity with the Rust reference
168
+
169
+ | Rust (v1.1.0) | Python |
170
+ |----------------|--------|
171
+ | `Packet::new / encode / decode` | `Packet.new / encode / decode` |
172
+ | `PacketType` | `PacketType` (`AUDIO_DATA`, …) |
173
+ | `AudioCodec` | `AudioCodec` (`OPUS`, `RAW_PCM_I16`, …) |
174
+ | `Packet::encode_audio_payload / decode_audio_payload` | same names, static methods |
175
+ | `MetadataBuilder` fluent API | identical fluent API |
176
+ | `Metadata {get,get_string,get_u32,get_u16,encode,decode,to_packet,…}` | identical methods |
177
+ | `AudioLevels` | `AudioLevels` |
178
+ | `FecEncoder::generate_repair / try_recover` | `FecEncoder.generate_repair / try_recover` |
179
+ | `JitterBuffer` | `JitterBuffer` |
180
+ | `Handshake` (roles/states, build/parse) | `Handshake` |
181
+ | `UdpTransport::bind / send_packet / recv_packet` | `UdpTransport.bind / send_packet / recv_packet` |
182
+ | `Connection`, `ConnectionPool` | `Connection`, `ConnectionPool` |
183
+ | `SessionManager` | `SessionManager` |
184
+ | `prelude` | `rushaudio.prelude` |
185
+ | `create_server(addr)` / `create_client()` | identical |
186
+
187
+ Naming intentionally follows PEP 8 in Python (e.g. `PacketType.AUDIO_DATA`,
188
+ `ConnectionState.STREAMING`) while behavior remains wire-identical.
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,171 @@
1
+ # RushAudio — Python port
2
+
3
+ A pure-Python, zero-dependency implementation of the [RushAudio](https://github.com/OseMine/rushaudio) protocol: a **binary protocol for low-latency audio streaming over UDP**.
4
+
5
+ This port mirrors the public API and wire format of the Rust reference implementation, so it is interoperable byte-for-byte on the wire.
6
+
7
+ | Property | Value |
8
+ |----------|-------|
9
+ | Transport | UDP (default port 4210) |
10
+ | Header size | 14 bytes |
11
+ | Max payload | 4096 bytes |
12
+ | Latency target | 30–100ms |
13
+ | Loss recovery | XOR FEC (recovers 1 loss per group) |
14
+ | Codecs | Opus, PCM S16LE, A-law, μ-law |
15
+ | Metadata | TLV key-value pairs (built-in + custom) |
16
+ | Byte order | Big-endian (except PCM samples: little-endian) |
17
+ | Python | ≥ 3.9, standard library only |
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install rushaudio # from PyPI (once published)
23
+ # or build/install from source:
24
+ cd ports/python
25
+ python -m pip wheel . -w dist # -> dist/rushaudio-1.1.0-py3-none-any.whl
26
+ pip install dist/rushaudio-1.1.0-py3-none-any.whl
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ import rushaudio
33
+ from rushaudio.prelude import *
34
+
35
+ server = rushaudio.create_server("0.0.0.0:4210") # receiver
36
+ client = rushaudio.create_client() # sender
37
+ peer = "127.0.0.1:4210"
38
+
39
+ # Encode/decode an audio frame (AudioData payload, spec §4.1)
40
+ payload = Packet.encode_audio_payload(2, 48000, AudioCodec.OPUS, opus_bytes)
41
+ pkt = Packet.new(PacketType.AUDIO_DATA, seq=1, ts=0, payload=payload)
42
+ client.send_packet(pkt, peer)
43
+
44
+ got = server.recv_packet() # non-blocking: None if nothing available
45
+ if got is not None:
46
+ packet, addr = got
47
+ ch, sr, codec, frame = Packet.decode_audio_payload(packet.payload)
48
+ ```
49
+
50
+ ## Metadata (TLV)
51
+
52
+ ```python
53
+ meta = (
54
+ MetadataBuilder()
55
+ .track_title("My Song")
56
+ .artist("Artist Name")
57
+ .album("Album")
58
+ .sample_rate(48000)
59
+ .channels(2)
60
+ .codec_info("Opus")
61
+ .bitrate(128000)
62
+ .custom(0x80, b"app-specific-data")
63
+ .build()
64
+ )
65
+ client.send_packet(meta.to_packet(seq=2, ts=20), peer)
66
+
67
+ received = Metadata.from_packet(packet) # on the other side
68
+ title = received.get_string(META_TRACK_TITLE) # -> "My Song"
69
+ ```
70
+
71
+ ## Handshake
72
+
73
+ ```python
74
+ config = StreamConfig(sample_rate=48000, channels=2, bitrate=128000, codec=AudioCodec.OPUS)
75
+ hs = Handshake(role=HandshakeRole.INITIATOR, ssrc=12345, config=config)
76
+ client.send_packet(hs.build_request(0, 0), peer)
77
+
78
+ # server side:
79
+ got = server.recv_packet()
80
+ remote_ssrc, cfg = Handshake.parse_request(got[0])
81
+ responder = Handshake(role=HandshakeRole.RESPONDER, ssrc=67890, config=cfg)
82
+ server.send_packet(responder.build_response(0, 0, accepted=True), got[1])
83
+ ```
84
+
85
+ ## FEC recovery
86
+
87
+ ```python
88
+ group = [pkt1, pkt2, pkt3] # original AudioData packets
89
+ repair = FecEncoder.generate_repair(group, seq=100, ts=0)
90
+ client.send_packet(repair, peer)
91
+
92
+ # receiver, having lost pkt2 of the group:
93
+ recovered = FecEncoder.try_recover(repair, [pkt1, pkt3]) # -> (2, b"...")
94
+ ```
95
+
96
+ ## Jitter buffer
97
+
98
+ ```python
99
+ jb = JitterBuffer() # 80ms target delay
100
+ jb.push(seq, ts, payload_bytes) # inserts in sequence order
101
+ out = jb.pop() # (ts, payload) once aged, else None
102
+ jb.adapt_delay() # clamp(2*jitter + 10ms, 20ms, 400ms)
103
+ stats = jb.stats() # depth, dropped, late, jitter
104
+ ```
105
+
106
+ ## Wire / raw bytes
107
+
108
+ ```python
109
+ wire = pkt.encode() # header + payload, 14+N bytes
110
+ parsed = Packet.decode(wire) # raises PacketError subclasses on invalid data
111
+ ```
112
+
113
+ `UdpTransport.recv_packet()` returns `(Packet, (host, port))` or `None` when
114
+ nothing is available (non-blocking). Non-parseable datagrams are skipped.
115
+
116
+ ## Layout
117
+
118
+ ```
119
+ ports/python/
120
+ ├── pyproject.toml
121
+ ├── rushaudio/
122
+ │ ├── __init__.py # public API: create_server / create_client / DEFAULT_PORT
123
+ │ ├── prelude.py # from rushaudio.prelude import *
124
+ │ ├── constants.py # magic, metadata keys, timing, FEC/level constants
125
+ │ ├── types.py # PacketType, AudioCodec, StreamConfig, StreamStats
126
+ │ ├── packet.py # 14-byte header, Packet encode/decode, errors
127
+ │ ├── metadata.py # Metadata / MetadataBuilder TLV codec
128
+ │ ├── levels.py # AudioLevels (VU meter) packet type
129
+ │ ├── fec.py # XOR FEC encode + single-loss recovery
130
+ │ ├── jitter.py # adaptive jitter buffer
131
+ │ ├── handshake.py # handshake state machine + payload builders
132
+ │ ├── transport.py # non-blocking UdpTransport
133
+ │ ├── connection.py # Connection + bounded ConnectionPool
134
+ │ └── session.py # SessionManager (keepalive, stale sweeping)
135
+ └── tests/
136
+ └── test_protocol.py # 37 tests (mirrors tests/integration.rs)
137
+ ```
138
+
139
+ ## Running the tests
140
+
141
+ ```bash
142
+ cd ports/python
143
+ python -m unittest discover -s tests -v # 37 tests, stdlib only
144
+ ```
145
+
146
+ ## Parity with the Rust reference
147
+
148
+ | Rust (v1.1.0) | Python |
149
+ |----------------|--------|
150
+ | `Packet::new / encode / decode` | `Packet.new / encode / decode` |
151
+ | `PacketType` | `PacketType` (`AUDIO_DATA`, …) |
152
+ | `AudioCodec` | `AudioCodec` (`OPUS`, `RAW_PCM_I16`, …) |
153
+ | `Packet::encode_audio_payload / decode_audio_payload` | same names, static methods |
154
+ | `MetadataBuilder` fluent API | identical fluent API |
155
+ | `Metadata {get,get_string,get_u32,get_u16,encode,decode,to_packet,…}` | identical methods |
156
+ | `AudioLevels` | `AudioLevels` |
157
+ | `FecEncoder::generate_repair / try_recover` | `FecEncoder.generate_repair / try_recover` |
158
+ | `JitterBuffer` | `JitterBuffer` |
159
+ | `Handshake` (roles/states, build/parse) | `Handshake` |
160
+ | `UdpTransport::bind / send_packet / recv_packet` | `UdpTransport.bind / send_packet / recv_packet` |
161
+ | `Connection`, `ConnectionPool` | `Connection`, `ConnectionPool` |
162
+ | `SessionManager` | `SessionManager` |
163
+ | `prelude` | `rushaudio.prelude` |
164
+ | `create_server(addr)` / `create_client()` | identical |
165
+
166
+ Naming intentionally follows PEP 8 in Python (e.g. `PacketType.AUDIO_DATA`,
167
+ `ConnectionState.STREAMING`) while behavior remains wire-identical.
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rushaudio"
7
+ version = "1.1.0"
8
+ description = "Low-latency audio over IP live streaming protocol (Python port)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "RushAudio Contributors" }]
13
+ keywords = ["audio", "streaming", "low-latency", "protocol", "udp"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Multimedia :: Sound/Audio",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/OseMine/rushaudio"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["rushaudio*"]
34
+
35
+ [tool.setuptools.package-data]
36
+ rushaudio = ["py.typed"]
@@ -0,0 +1,94 @@
1
+ """RushAudio — low-latency audio over IP live streaming protocol.
2
+
3
+ Pure-Python, zero-dependency port of the Rust reference implementation
4
+ (https://github.com/OseMine/rushaudio). Follows the canonical wire spec in
5
+ ``docs/protocol.md`` of the rushaudio repository.
6
+
7
+ Public API mirrors the Rust crate:
8
+
9
+ import rushaudio
10
+ server = rushaudio.create_server("0.0.0.0:4210")
11
+ client = rushaudio.create_client()
12
+
13
+ pkt = Packet.new(PacketType.AUDIO_DATA, seq=1, ts=0,
14
+ payload=b"...")
15
+ wire = pkt.encode()
16
+ parsed = Packet.decode(wire)
17
+ """
18
+
19
+ from . import constants as _c
20
+ from .connection import Connection, ConnectionPool
21
+ from .fec import FecEncoder
22
+ from .handshake import Handshake, HandshakeRole, HandshakeState
23
+ from .jitter import JitterBuffer, JitterStats
24
+ from .levels import AudioLevels
25
+ from .metadata import Metadata, MetadataBuilder, MetadataEntry, MetadataMap
26
+ from .packet import (
27
+ BadMagicError,
28
+ InvalidPayloadError,
29
+ Packet,
30
+ PacketError,
31
+ PacketHeader,
32
+ PacketTooShortError,
33
+ TruncatedPayloadError,
34
+ UnknownTypeError,
35
+ )
36
+ from .session import Session, SessionManager
37
+ from .transport import UdpTransport
38
+ from .types import (
39
+ AudioCodec,
40
+ ConnectionState,
41
+ PacketType,
42
+ StreamConfig,
43
+ StreamStats,
44
+ )
45
+
46
+ VERSION = "1.1.0"
47
+ DEFAULT_PORT: int = 4210
48
+
49
+
50
+ def create_server(bind_addr: str) -> UdpTransport:
51
+ """Bind a non-blocking UDP server, e.g. ``create_server("0.0.0.0:4210")``."""
52
+ return UdpTransport.bind(bind_addr)
53
+
54
+
55
+ def create_client() -> UdpTransport:
56
+ """Bind a non-blocking UDP socket on an ephemeral port."""
57
+ return UdpTransport.bind("0.0.0.0:0")
58
+
59
+
60
+ __all__ = [
61
+ "AudioCodec",
62
+ "AudioLevels",
63
+ "BadMagicError",
64
+ "Connection",
65
+ "ConnectionPool",
66
+ "ConnectionState",
67
+ "FecEncoder",
68
+ "Handshake",
69
+ "HandshakeRole",
70
+ "HandshakeState",
71
+ "InvalidPayloadError",
72
+ "JitterBuffer",
73
+ "JitterStats",
74
+ "Metadata",
75
+ "MetadataBuilder",
76
+ "MetadataEntry",
77
+ "MetadataMap",
78
+ "Packet",
79
+ "PacketError",
80
+ "PacketHeader",
81
+ "PacketTooShortError",
82
+ "PacketType",
83
+ "Session",
84
+ "SessionManager",
85
+ "StreamConfig",
86
+ "StreamStats",
87
+ "TruncatedPayloadError",
88
+ "UdpTransport",
89
+ "UnknownTypeError",
90
+ "VERSION",
91
+ "DEFAULT_PORT",
92
+ "create_server",
93
+ "create_client",
94
+ ]
@@ -0,0 +1,99 @@
1
+ """Per-peer connection tracking and a bounded connection pool.
2
+
3
+ Mirrors ``src/transport/connection.rs`` in the Rust reference implementation.
4
+ """
5
+
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Hashable, Iterator, List, Optional
9
+
10
+ from .types import ConnectionState, StreamConfig, StreamStats
11
+
12
+
13
+ @dataclass
14
+ class Connection:
15
+ """State and statistics for one remote peer."""
16
+
17
+ remote_addr: Hashable
18
+ state: ConnectionState = ConnectionState.DISCONNECTED
19
+ config: StreamConfig = field(default_factory=StreamConfig)
20
+ stats: StreamStats = field(default_factory=StreamStats)
21
+ sequence_number: int = 0
22
+ connected_at: Optional[float] = None
23
+ last_activity: float = field(default_factory=time.monotonic)
24
+ ssrc: int = 0
25
+
26
+ def next_seq(self) -> int:
27
+ """Return the current sequence number and advance it (mod 2^32)."""
28
+ value = self.sequence_number
29
+ self.sequence_number = (self.sequence_number + 1) & 0xFFFFFFFF
30
+ return value
31
+
32
+ def current_seq(self) -> int:
33
+ return self.sequence_number
34
+
35
+ def mark_activity(self) -> None:
36
+ self.last_activity = time.monotonic()
37
+
38
+ def elapsed_since_activity(self) -> float:
39
+ return time.monotonic() - self.last_activity
40
+
41
+ def set_state(self, state: ConnectionState) -> None:
42
+ self.state = state
43
+ if state == ConnectionState.CONNECTED:
44
+ self.connected_at = time.monotonic()
45
+
46
+ def record_sent(self, size: int) -> None:
47
+ self.stats.packets_sent += 1
48
+ self.stats.bytes_sent += size
49
+
50
+ def record_received(self, size: int) -> None:
51
+ self.stats.packets_received += 1
52
+ self.stats.bytes_received += size
53
+
54
+ def record_loss(self, count: int) -> None:
55
+ self.stats.packets_lost += count
56
+
57
+
58
+ class ConnectionPool:
59
+ """A bounded collection of connections keyed by remote address."""
60
+
61
+ def __init__(self, max_connections: int) -> None:
62
+ self._connections: List[Connection] = []
63
+ self._max_connections = max_connections
64
+
65
+ def get(self, addr: Hashable) -> Optional[Connection]:
66
+ for conn in self._connections:
67
+ if conn.remote_addr == addr:
68
+ return conn
69
+ return None
70
+
71
+ def get_mut(self, addr: Hashable) -> Optional[Connection]:
72
+ return self.get(addr)
73
+
74
+ def add(self, conn: Connection) -> bool:
75
+ if len(self._connections) >= self._max_connections:
76
+ return False
77
+ self._connections.append(conn)
78
+ return True
79
+
80
+ def remove(self, addr: Hashable) -> None:
81
+ self._connections = [c for c in self._connections if c.remote_addr != addr]
82
+
83
+ def len(self) -> int:
84
+ return len(self._connections)
85
+
86
+ def __len__(self) -> int:
87
+ return len(self._connections)
88
+
89
+ def is_empty(self) -> bool:
90
+ return not self._connections
91
+
92
+ def iter(self) -> Iterator[Connection]:
93
+ return iter(self._connections)
94
+
95
+ def __iter__(self) -> Iterator[Connection]:
96
+ return iter(self._connections)
97
+
98
+ def contains(self, addr: Hashable) -> bool:
99
+ return any(c.remote_addr == addr for c in self._connections)
@@ -0,0 +1,62 @@
1
+ """Protocol constants for the RushAudio Python port.
2
+
3
+ Mirrors ``src/protocol/constants.rs`` in the Rust reference implementation.
4
+ """
5
+
6
+ PROTOCOL_MAGIC = bytes((0x52, 0x41)) # ASCII "RA"
7
+ PROTOCOL_VERSION = 1
8
+
9
+ HEADER_SIZE = 14 # 2(magic) + 1(ver) + 1(type) + 4(seq) + 4(ts) + 2(len)
10
+ MAX_PAYLOAD_SIZE = 4096
11
+ MAX_PACKET_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE
12
+
13
+ # Default timing
14
+ DEFAULT_SAMPLE_RATE = 48000
15
+ DEFAULT_FRAME_DURATION_MS = 20
16
+ DEFAULT_JITTER_BUFFER_MS = 80
17
+ DEFAULT_KEEPALIVE_INTERVAL_MS = 5000
18
+ DEFAULT_HANDSHAKE_TIMEOUT_MS = 3000
19
+
20
+ # Opus defaults
21
+ OPUS_FRAME_SIZE_20MS = 960 # 48000 * 0.020
22
+ OPUS_CHANNELS = 2
23
+
24
+ # FEC
25
+ FEC_REDUNDANCY_COUNT = 1
26
+ FEC_GROUP_SIZE = 4
27
+
28
+ # Control codes
29
+ CONTROL_STREAM_START = 0x01
30
+ CONTROL_STREAM_STOP = 0x02
31
+ CONTROL_STREAM_PAUSE = 0x03
32
+ CONTROL_STREAM_RESUME = 0x04
33
+
34
+ # Metadata entry header: 1(key) + 2(length) = 3 bytes
35
+ METADATA_ENTRY_HEADER_SIZE = 3
36
+
37
+ # Built-in metadata keys (0x00 reserved, 0x01-0x7F reserved by protocol)
38
+ META_SSRC = 0x01
39
+ META_TRACK_TITLE = 0x02
40
+ META_ARTIST = 0x03
41
+ META_ALBUM = 0x04
42
+ META_GENRE = 0x05
43
+ META_SAMPLE_RATE = 0x06
44
+ META_CHANNELS = 0x07
45
+ META_CODEC_INFO = 0x08
46
+ META_BITRATE = 0x09
47
+ META_DURATION_MS = 0x0A
48
+ META_STREAM_TITLE = 0x0B
49
+ META_STREAM_URL = 0x0C
50
+
51
+ # Custom metadata keys start at 0x80
52
+ META_CUSTOM_BASE = 0x80
53
+
54
+ # Audio level / VU meter
55
+ # Payload: 4(audio_sequence) + 4(audio_timestamp) + 1(peak) + 1(rms)
56
+ AUDIO_LEVELS_PAYLOAD_SIZE = 10
57
+ # Levels are encoded in dBFS with 1 dB per unit. 0 = full scale (0 dBFS),
58
+ # -127 = quietest non-silent level. LEVEL_SILENCE (-128) means -infinity dBFS
59
+ # (digital silence).
60
+ LEVEL_DBFS_MAX = 0
61
+ LEVEL_DBFS_MIN = -127
62
+ LEVEL_SILENCE = -128