Commit | Line | Data |
---|---|---|
1f0de05c | 1 | # coding=utf-8 |
094e4ad1 | 2 | ''' |
6b27160f | 3 | Copyright (C) 2012 Juho Vähä-Herttua |
094e4ad1 | 4 | |
6b27160f JVH |
5 | Permission is hereby granted, free of charge, to any person obtaining |
6 | a copy of this software and associated documentation files (the | |
7 | "Software"), to deal in the Software without restriction, including | |
8 | without limitation the rights to use, copy, modify, merge, publish, | |
9 | distribute, sublicense, and/or sell copies of the Software, and to | |
10 | permit persons to whom the Software is furnished to do so, subject to | |
11 | the following conditions: | |
094e4ad1 | 12 | |
6b27160f JVH |
13 | The above copyright notice and this permission notice shall be included |
14 | in all copies or substantial portions of the Software. | |
15 | ||
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
094e4ad1 JVH |
23 | ''' |
24 | ||
8be1424a JVH |
25 | import os |
26 | import sys | |
27 | import platform | |
28 | ||
29 | from ctypes import * | |
30 | ||
46cb8988 JVH |
31 | RSA_KEY = """ |
32 | -----BEGIN RSA PRIVATE KEY----- | |
33 | MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt | |
34 | wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U | |
35 | wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf | |
36 | /+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/ | |
37 | UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW | |
38 | BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa | |
39 | LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5 | |
40 | NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm | |
41 | lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz | |
42 | aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu | |
43 | a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM | |
44 | oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z | |
45 | oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+ | |
46 | k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL | |
47 | AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA | |
48 | cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf | |
49 | 54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov | |
50 | 17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc | |
51 | 1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI | |
52 | LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ | |
53 | 2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY= | |
54 | -----END RSA PRIVATE KEY----- | |
55 | """ | |
56 | ||
10ceb254 JVH |
57 | class RaopLogLevel: |
58 | EMERG = 0 | |
59 | ALERT = 1 | |
60 | CRIT = 2 | |
61 | ERR = 3 | |
62 | WARNING = 4 | |
63 | NOTICE = 5 | |
64 | INFO = 6 | |
65 | DEBUG = 7 | |
66 | ||
ebc7ed4d | 67 | raop_log_callback_prototype = CFUNCTYPE(None, c_void_p, c_int, c_char_p) |
10ceb254 | 68 | |
23ab90af JVH |
69 | audio_init_prototype = CFUNCTYPE(py_object, c_void_p, c_int, c_int, c_int) |
70 | audio_process_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_int) | |
71 | audio_destroy_prototype = CFUNCTYPE(None, c_void_p, c_void_p) | |
72 | ||
73 | audio_flush_prototype = CFUNCTYPE(None, c_void_p, c_void_p) | |
74 | audio_set_volume_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_float) | |
75 | audio_set_metadata_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_int) | |
76 | audio_set_coverart_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_int) | |
8be1424a JVH |
77 | |
78 | class RaopNativeCallbacks(Structure): | |
23ab90af JVH |
79 | _fields_ = [("cls", py_object), |
80 | ("audio_init", audio_init_prototype), | |
81 | ("audio_process", audio_process_prototype), | |
82 | ("audio_destroy", audio_destroy_prototype), | |
83 | ("audio_flush", audio_flush_prototype), | |
84 | ("audio_set_volume", audio_set_volume_prototype), | |
85 | ("audio_set_metadata", audio_set_metadata_prototype), | |
86 | ("audio_set_coverart", audio_set_coverart_prototype)] | |
8be1424a | 87 | |
c1b1e2c3 | 88 | def InitShairplay(libshairplay): |
8be1424a JVH |
89 | # Initialize dnssd related functions |
90 | libshairplay.dnssd_init.restype = c_void_p | |
094e4ad1 | 91 | libshairplay.dnssd_init.argtypes = [POINTER(c_int)] |
8be1424a | 92 | libshairplay.dnssd_register_raop.restype = c_int |
a68fedbb | 93 | libshairplay.dnssd_register_raop.argtypes = [c_void_p, c_char_p, c_ushort, POINTER(c_char), c_int, c_int] |
8be1424a | 94 | libshairplay.dnssd_register_airplay.restype = c_int |
094e4ad1 | 95 | libshairplay.dnssd_register_airplay.argtypes = [c_void_p, c_char_p, c_ushort, POINTER(c_char), c_int] |
8be1424a JVH |
96 | libshairplay.dnssd_unregister_raop.restype = None |
97 | libshairplay.dnssd_unregister_raop.argtypes = [c_void_p] | |
98 | libshairplay.dnssd_unregister_airplay.restype = None | |
99 | libshairplay.dnssd_unregister_airplay.argtypes = [c_void_p] | |
100 | libshairplay.dnssd_destroy.restype = None | |
101 | libshairplay.dnssd_destroy.argtypes = [c_void_p] | |
102 | ||
103 | # Initialize raop related functions | |
104 | libshairplay.raop_init.restype = c_void_p | |
ebc7ed4d | 105 | libshairplay.raop_init.argtypes = [c_int, POINTER(RaopNativeCallbacks), c_char_p, POINTER(c_int)] |
10ceb254 JVH |
106 | libshairplay.raop_set_log_level.restype = None |
107 | libshairplay.raop_set_log_level.argtypes = [c_void_p, c_int] | |
108 | libshairplay.raop_set_log_callback.restype = None | |
ebc7ed4d | 109 | libshairplay.raop_set_log_callback.argtypes = [c_void_p, raop_log_callback_prototype, c_void_p] |
2528f9b0 JVH |
110 | libshairplay.raop_is_running.restype = c_int |
111 | libshairplay.raop_is_running.argtypes = [c_void_p] | |
8be1424a | 112 | libshairplay.raop_start.restype = c_int |
a68fedbb | 113 | libshairplay.raop_start.argtypes = [c_void_p, POINTER(c_ushort), POINTER(c_char), c_int, c_char_p] |
8be1424a JVH |
114 | libshairplay.raop_stop.restype = None |
115 | libshairplay.raop_stop.argtypes = [c_void_p] | |
116 | libshairplay.raop_destroy.restype = None | |
117 | libshairplay.raop_destroy.argtypes = [c_void_p] | |
118 | ||
c1b1e2c3 JVH |
119 | def LoadShairplay(path): |
120 | if sys.maxsize < 2**32: | |
121 | libname = "shairplay32" | |
122 | else: | |
123 | libname = "shairplay64" | |
124 | ||
125 | if platform.system() == "Windows": | |
126 | libname = libname + ".dll" | |
127 | elif platform.system() == "Darwin": | |
128 | libname = "lib" + libname + ".dylib" | |
129 | else: | |
130 | libname = "lib" + libname + ".so" | |
131 | ||
132 | try: | |
133 | fullpath = os.path.join(path, libname) | |
134 | libshairplay = cdll.LoadLibrary(fullpath) | |
135 | except: | |
136 | raise RuntimeError("Couldn't load shairplay library " + libname) | |
137 | ||
138 | InitShairplay(libshairplay) | |
8be1424a JVH |
139 | return libshairplay |
140 | ||
8be1424a JVH |
141 | class RaopCallbacks: |
142 | def audio_init(self, bits, channels, samplerate): | |
143 | raise NotImplementedError() | |
144 | ||
8be1424a JVH |
145 | def audio_process(self, session, buffer): |
146 | raise NotImplementedError() | |
147 | ||
23ab90af JVH |
148 | def audio_destroy(self, session): |
149 | raise NotImplementedError() | |
150 | ||
151 | def audio_set_volume(self, session, volume): | |
152 | pass | |
153 | ||
8be1424a JVH |
154 | def audio_flush(self, session): |
155 | pass | |
156 | ||
23ab90af JVH |
157 | def audio_set_metadata(self, session, buffer): |
158 | pass | |
159 | ||
160 | def audio_set_coverart(self, session, buffer): | |
8be1424a JVH |
161 | pass |
162 | ||
163 | class RaopService: | |
164 | def audio_init_cb(self, cls, bits, channels, samplerate): | |
165 | session = self.callbacks.audio_init(bits, channels, samplerate) | |
1f0de05c JVH |
166 | self.sessions.append(session) |
167 | return session | |
8be1424a | 168 | |
8be1424a JVH |
169 | def audio_process_cb(self, cls, sessionptr, buffer, buflen): |
170 | session = cast(sessionptr, py_object).value | |
171 | strbuffer = string_at(buffer, buflen) | |
172 | self.callbacks.audio_process(session, strbuffer) | |
173 | ||
8be1424a JVH |
174 | def audio_destroy_cb(self, cls, sessionptr): |
175 | session = cast(sessionptr, py_object).value | |
176 | self.callbacks.audio_destroy(session) | |
1f0de05c JVH |
177 | if session in self.sessions: |
178 | self.sessions.remove(session) | |
8be1424a | 179 | |
23ab90af JVH |
180 | def audio_flush_cb(self, cls, sessionptr): |
181 | session = cast(sessionptr, py_object).value | |
182 | self.callbacks.audio_flush(session) | |
183 | ||
184 | def audio_set_volume_cb(self, cls, sessionptr, volume): | |
185 | session = cast(sessionptr, py_object).value | |
186 | self.callbacks.audio_set_volume(session, volume) | |
8be1424a | 187 | |
23ab90af JVH |
188 | def audio_set_metadata_cb(self, cls, sessionptr, buffer, buflen): |
189 | session = cast(sessionptr, py_object).value | |
190 | strbuffer = string_at(buffer, buflen) | |
191 | self.callbacks.audio_set_metadata(session, strbuffer) | |
192 | ||
193 | def audio_set_coverart_cb(self, cls, sessionptr, buffer, buflen): | |
194 | session = cast(sessionptr, py_object).value | |
195 | strbuffer = string_at(buffer, buflen) | |
196 | self.callbacks.audio_set_coverart(session, strbuffer) | |
197 | ||
198 | ||
199 | def __init__(self, libshairplay, max_clients, callbacks): | |
8be1424a JVH |
200 | self.libshairplay = libshairplay |
201 | self.callbacks = callbacks | |
1f0de05c | 202 | self.sessions = [] |
8be1424a JVH |
203 | self.instance = None |
204 | ||
205 | # We need to hold a reference to native_callbacks | |
206 | self.native_callbacks = RaopNativeCallbacks() | |
207 | self.native_callbacks.audio_init = audio_init_prototype(self.audio_init_cb) | |
8be1424a | 208 | self.native_callbacks.audio_process = audio_process_prototype(self.audio_process_cb) |
8be1424a | 209 | self.native_callbacks.audio_destroy = audio_destroy_prototype(self.audio_destroy_cb) |
23ab90af JVH |
210 | self.native_callbacks.audio_flush = audio_flush_prototype(self.audio_flush_cb) |
211 | self.native_callbacks.audio_set_volume = audio_set_volume_prototype(self.audio_set_volume_cb) | |
212 | self.native_callbacks.audio_set_metadata = audio_set_metadata_prototype(self.audio_set_metadata_cb) | |
213 | self.native_callbacks.audio_set_coverart = audio_set_coverart_prototype(self.audio_set_coverart_cb) | |
8be1424a JVH |
214 | |
215 | # Initialize the raop instance with our callbacks | |
ebc7ed4d | 216 | self.instance = self.libshairplay.raop_init(max_clients, pointer(self.native_callbacks), RSA_KEY, None) |
8be1424a JVH |
217 | if self.instance == None: |
218 | raise RuntimeError("Initializing library failed") | |
219 | ||
220 | def __del__(self): | |
221 | if self.instance != None: | |
222 | self.libshairplay.raop_destroy(self.instance) | |
223 | self.instance = None | |
224 | ||
2528f9b0 JVH |
225 | def is_running(self): |
226 | if self.libshairplay.raop_is_running(self.instance): | |
227 | return True | |
228 | else: | |
229 | return False | |
230 | ||
10ceb254 JVH |
231 | def set_log_level(self, level): |
232 | self.libshairplay.raop_set_log_level(self.instance, level) | |
233 | ||
234 | def set_log_callback(self, log_callback): | |
ebc7ed4d JVH |
235 | # Create a new callback function for thread safety |
236 | def log_callback_cb(cls, level, message): | |
237 | log_callback(level, message) | |
238 | ||
ea824022 | 239 | # We need to hold a reference to the log callback instance |
ebc7ed4d JVH |
240 | log_callback_ptr = raop_log_callback_prototype(log_callback_cb) |
241 | self.libshairplay.raop_set_log_callback(self.instance, log_callback_ptr, None) | |
242 | self.log_callback = log_callback_ptr | |
10ceb254 | 243 | |
a68fedbb | 244 | def start(self, port, hwaddrstr, password=None): |
8be1424a JVH |
245 | port = c_ushort(port) |
246 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) | |
247 | ||
a68fedbb | 248 | ret = self.libshairplay.raop_start(self.instance, pointer(port), hwaddr, c_int(len(hwaddr)), password) |
8be1424a JVH |
249 | if ret < 0: |
250 | raise RuntimeError("Starting RAOP instance failed") | |
251 | return port.value | |
252 | ||
253 | def stop(self): | |
254 | self.libshairplay.raop_stop(self.instance) | |
255 | ||
256 | class DnssdService: | |
257 | def __init__(self, libshairplay): | |
258 | self.libshairplay = libshairplay | |
259 | self.instance = None | |
260 | ||
8be1424a JVH |
261 | error = c_int(0) |
262 | ||
094e4ad1 | 263 | self.instance = self.libshairplay.dnssd_init(pointer(error)) |
8be1424a JVH |
264 | if self.instance == None: |
265 | raise RuntimeError("Initializing library failed: " + str(error.value)) | |
266 | ||
267 | def __del__(self): | |
268 | if self.instance != None: | |
269 | self.libshairplay.dnssd_destroy(self.instance) | |
270 | self.instance = None | |
271 | ||
a68fedbb | 272 | def register_raop(self, name, port, hwaddrstr, password=False): |
094e4ad1 | 273 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) |
a68fedbb JVH |
274 | use_pw = c_int(0) |
275 | if password: | |
276 | use_pw = c_int(1) | |
277 | self.libshairplay.dnssd_register_raop(self.instance, name, c_ushort(port), hwaddr, len(hwaddr), use_pw) | |
8be1424a JVH |
278 | |
279 | def unregister_raop(self): | |
280 | self.libshairplay.dnssd_unregister_raop(self.instance) | |
281 | ||
094e4ad1 JVH |
282 | def register_airplay(self, name, port, hwaddrstr): |
283 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) | |
284 | self.libshairplay.dnssd_register_airplay(self.instance, name, c_ushort(port), hwaddr, len(hwaddr)) | |
8be1424a JVH |
285 | |
286 | def unregister_airplay(self): | |
287 | self.libshairplay.dnssd_unregister_airplay(self.instance) | |
288 |