Skip to content

Commit 0e2c677

Browse files
committed
initial
0 parents  commit 0e2c677

File tree

6 files changed

+1565
-0
lines changed

6 files changed

+1565
-0
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Jamulus python library
2+
3+
The library supports sending and receiving of [Jamulus](https://github.com/jamulussoftware/jamulus/) protocol messages.
4+
5+
## Usage example
6+
7+
* Fetch server list from central (directory) server
8+
9+
```python
10+
import jamulus
11+
12+
server = ("<hostname>", jamulus.DEFAULT_PORT)
13+
14+
jc = jamulus.JamulusConnector()
15+
jc.sendto(server, "CLM_REQ_SERVER_LIST")
16+
17+
try:
18+
while True:
19+
addr, key, count, values = jc.recvfrom(timeout=1)
20+
if key == "CLM_SERVER_LIST":
21+
for server in values:
22+
print(f'{server["name"]} ({server["max_clients"]})')
23+
except TimeoutError:
24+
pass
25+
```
26+
27+
## Scripts
28+
29+
### `central_server.py`
30+
31+
* Simple implementation of a _Jamulus Central Server_
32+
* _Jamulus Servers_ can register / unregister
33+
* _Jamulus Clients_ can get list of registered servers
34+
35+
### `central_proxy.py`
36+
37+
* Collect server lists from multiple _Jamulus Central Servers_
38+
* Filters servers by their country ID
39+
* _Jamulus Clients_ can get filtered list of servers
40+
41+
### `dummy_server.py`
42+
43+
* Simulates a _Jamulus Server_
44+
45+
### `dummy_client.py`
46+
47+
* Simulates a _Jamulus Client_ connecting to a _Jamulus Server_
48+
49+
## Limitations
50+
51+
* The implementation is not proven / tested to be 100% reliable
52+
* Certain exceptions are not handled and can crash the process
53+
54+
## References
55+
56+
These projects were very helpful to understand the Jamulus protocol:
57+
- [softins/jamulus-php](https://github.com/softins/jamulus-php)
58+
- [softins/jamulus-wireshark](https://github.com/softins/jamulus-wireshark)

central_proxy.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/python3
2+
3+
import jamulus
4+
5+
import argparse
6+
import signal
7+
import sys
8+
9+
from time import time
10+
11+
12+
DEFAULT_INTERVAL = 300
13+
14+
15+
class ServerList(dict):
16+
def format_server(server):
17+
age_seconds = int(time() - server["time_updated"]) if "time_updated" in server.keys() else "?"
18+
return "{:>15}:{:<5} {} {:<20} {:>3}/{:>3} {}/{} ({}/{}) {}s {}".format(
19+
server.get("ip", 0),
20+
server.get("port", 0),
21+
"*" if server.get("permanent", 0) == 1 else " ",
22+
server.get("name", "?"),
23+
server.get("clients", "?"),
24+
server.get("max_clients", "?"),
25+
server.get("city", "?"),
26+
jamulus.COUNTRY_KEYS.get(server.get("country_id"), "?"),
27+
jamulus.OS_KEYS.get(server.get("os"), "?"),
28+
server.get("version", "?"),
29+
age_seconds,
30+
server.get("internal_address", ""),
31+
)
32+
33+
def __str__(self):
34+
return "\n".join(list(map(ServerList.format_server, self.values())))
35+
36+
def update_server(self, key, values):
37+
if key in self.keys():
38+
self[key].update(values)
39+
self[key]["time_updated"] = time()
40+
41+
def create_or_update_server(self, key, values):
42+
if key not in self.keys():
43+
self[key] = {"time_created": time()}
44+
self.update_server(key, values)
45+
46+
def add_single(self, source_host, server):
47+
server["ip"] = source_host[0]
48+
key = (server["ip"], server["port"])
49+
self.create_or_update_server(key, server)
50+
print(ServerList.format_server(self[key]))
51+
52+
def add_list(self, source_host, server_list):
53+
for server in server_list:
54+
if server["ip"] == "0.0.0.0":
55+
# central servers first (own) entry
56+
server["ip"], server["port"] = source_host
57+
server["source_host"] = source_host
58+
key = (server["ip"], server["port"])
59+
self.create_or_update_server(key, server)
60+
61+
def remove_server(self, key):
62+
if key in self.keys():
63+
print(ServerList.format_server(self[key]))
64+
del self[key]
65+
66+
def get_list(self, add_dummy=True):
67+
server_list = list(self.values())
68+
if add_dummy:
69+
server_list.insert(
70+
0,
71+
{
72+
"ip": "0.0.0.0",
73+
"port": 0,
74+
"country_id": 0,
75+
"max_clients": 0,
76+
"permanent": 1,
77+
"name": "Jamulus Proxy",
78+
"internal_address": "",
79+
"city": "",
80+
},
81+
)
82+
return server_list
83+
84+
def filter(self, country_ids):
85+
if len(country_ids) > 0:
86+
filtered = dict((k, s) for k, s in super().items() if s["country_id"] in country_ids)
87+
super().clear()
88+
super().update(filtered)
89+
90+
def copy(self):
91+
return ServerList(super().copy())
92+
93+
94+
class ActionScheduler:
95+
def __init__(self, jamulus, central_servers, interval):
96+
self.jamulus = jamulus
97+
self.central_servers = central_servers
98+
self.interval = interval
99+
self.next_action = time()
100+
101+
def run(self):
102+
# request server lists
103+
if self.next_action <= time():
104+
print("request server lists")
105+
106+
for addr in self.central_servers:
107+
self.jamulus.sendto(addr, "CLM_REQ_SERVER_LIST")
108+
109+
self.next_action += self.interval
110+
111+
# return maximum time after which the scheduler needs to run again
112+
return self.next_action - time()
113+
114+
115+
def argument_parser():
116+
def server_argument(string):
117+
server = string.split(":")
118+
if len(server) == 1:
119+
server.append(jamulus.DEFAULT_PORT)
120+
elif len(server) == 2:
121+
server[1] = int(server[1])
122+
else:
123+
raise ValueError
124+
return tuple(server)
125+
126+
parser = argparse.ArgumentParser()
127+
parser.add_argument("--port", type=int, default=jamulus.DEFAULT_PORT, help="local port number")
128+
parser.add_argument(
129+
"--centralserver",
130+
type=server_argument,
131+
required=True,
132+
action="extend",
133+
nargs="+",
134+
default=[],
135+
help="central servers for collecting server lists",
136+
)
137+
parser.add_argument(
138+
"--interval",
139+
type=int,
140+
default=DEFAULT_INTERVAL,
141+
help="central server collection interval",
142+
)
143+
parser.add_argument(
144+
"--filter",
145+
type=int,
146+
action="extend",
147+
nargs="+",
148+
default=[],
149+
help="country IDs to filter",
150+
)
151+
parser.add_argument(
152+
"--debug",
153+
action="store_true",
154+
help="print protocol data",
155+
)
156+
return parser.parse_args()
157+
158+
159+
################################################################################
160+
161+
162+
def main():
163+
# get arguments
164+
args = argument_parser()
165+
166+
# create jamulus connector
167+
jc = jamulus.JamulusConnector(port=args.port, debug=args.debug)
168+
169+
# create empty server list
170+
server_list = ServerList()
171+
172+
# initiate repeated actions
173+
scheduler = ActionScheduler(
174+
jamulus=jc,
175+
central_servers=args.centralserver,
176+
interval=args.interval,
177+
)
178+
179+
# receive messages indefinitely
180+
while True:
181+
timeout = scheduler.run()
182+
183+
if timeout is not None and timeout <= 0:
184+
print("negative timeout {}".format(timeout))
185+
continue
186+
187+
try:
188+
addr, key, count, values = jc.recvfrom(timeout)
189+
except TimeoutError:
190+
continue
191+
192+
if key == "AUDIO":
193+
# stop clients from connecting
194+
jc.sendto(addr, "CLM_DISCONNECTION")
195+
196+
elif key == "CLM_SERVER_LIST":
197+
# add servers to list
198+
print("add/update {} servers".format(len(values)))
199+
server_list.add_list(addr, values)
200+
201+
elif key == "CLM_REQ_SERVER_LIST":
202+
# get and filter server list
203+
server_list_send = server_list.copy()
204+
server_list_send.filter(args.filter)
205+
206+
# send (filtered) server list
207+
print("sending {} servers\n{}".format(len(server_list_send), server_list_send))
208+
jc.sendto(addr, "CLM_SERVER_LIST", server_list_send.get_list())
209+
210+
211+
def signal_handler(sig, frame):
212+
print()
213+
sys.exit(0)
214+
215+
216+
if __name__ == "__main__":
217+
signal.signal(signal.SIGINT, signal_handler)
218+
main()

central_server.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/python3
2+
3+
import jamulus
4+
5+
import argparse
6+
import signal
7+
import sys
8+
9+
10+
def argument_parser():
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument("--port", type=int, default=jamulus.DEFAULT_PORT, help="local port number")
13+
parser.add_argument(
14+
"--debug",
15+
action="store_true",
16+
help="print protocol data",
17+
)
18+
return parser.parse_args()
19+
20+
21+
def main():
22+
# get arguments
23+
args = argument_parser()
24+
25+
# create jamulus connector
26+
jc = jamulus.JamulusConnector(port=args.port, debug=args.debug)
27+
28+
# create empty server list
29+
server_list = {}
30+
31+
# receive messages indefinitely
32+
while True:
33+
addr, key, count, values = jc.recvfrom()
34+
35+
if key == "AUDIO":
36+
# stop clients from connecting
37+
jc.sendto(addr, "CLM_DISCONNECTION")
38+
39+
elif key in ["CLM_REGISTER_SERVER", "CLM_REGISTER_SERVER_EX"]:
40+
# add server to list
41+
values["ip"] = addr[0]
42+
server_list[addr] = values
43+
44+
print("registering server\n{}".format(values))
45+
46+
# send successful registration response
47+
jc.sendto(addr, "CLM_REGISTER_SERVER_RESP", {"status": 0})
48+
49+
elif key == "CLM_UNREGISTER_SERVER":
50+
print("unregistering server")
51+
52+
# remove server from list
53+
if addr in server_list.keys():
54+
del server_list[addr]
55+
56+
elif key == "CLM_REQ_SERVER_LIST":
57+
# send server list with dummy entry in first position
58+
server_list_send = [
59+
{
60+
"ip": "0.0.0.0",
61+
"port": 0,
62+
"country_id": 0,
63+
"max_clients": 0,
64+
"permanent": 0,
65+
"name": "",
66+
"internal_address": "",
67+
"city": "",
68+
}
69+
] + list(server_list.values())
70+
print("sending {} servers\n{}".format(len(server_list_send), server_list_send))
71+
jc.sendto(addr, "CLM_SERVER_LIST", server_list_send)
72+
73+
74+
def signal_handler(sig, frame):
75+
print()
76+
sys.exit(0)
77+
78+
79+
if __name__ == "__main__":
80+
signal.signal(signal.SIGINT, signal_handler)
81+
main()

0 commit comments

Comments
 (0)