| 1 | # coding=utf-8 |
| 2 | ''' |
| 3 | Copyright (C) 2012 Juho Vähä-Herttua |
| 4 | |
| 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: |
| 12 | |
| 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. |
| 23 | ''' |
| 24 | |
| 25 | import os |
| 26 | import sys |
| 27 | import platform |
| 28 | |
| 29 | from ctypes import * |
| 30 | |
| 31 | audio_init_prototype = CFUNCTYPE(py_object, c_void_p, c_int, c_int, c_int) |
| 32 | audio_set_volume_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_float) |
| 33 | audio_process_prototype = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_int) |
| 34 | audio_flush_prototype = CFUNCTYPE(None, c_void_p, c_void_p) |
| 35 | audio_destroy_prototype = CFUNCTYPE(None, c_void_p, c_void_p) |
| 36 | |
| 37 | class RaopNativeCallbacks(Structure): |
| 38 | _fields_ = [("cls", py_object), |
| 39 | ("audio_init", audio_init_prototype), |
| 40 | ("audio_set_volume", audio_set_volume_prototype), |
| 41 | ("audio_process", audio_process_prototype), |
| 42 | ("audio_flush", audio_flush_prototype), |
| 43 | ("audio_destroy", audio_destroy_prototype)] |
| 44 | |
| 45 | def InitShairplay(libshairplay): |
| 46 | # Initialize dnssd related functions |
| 47 | libshairplay.dnssd_init.restype = c_void_p |
| 48 | libshairplay.dnssd_init.argtypes = [POINTER(c_int)] |
| 49 | libshairplay.dnssd_register_raop.restype = c_int |
| 50 | libshairplay.dnssd_register_raop.argtypes = [c_void_p, c_char_p, c_ushort, POINTER(c_char), c_int, c_int] |
| 51 | libshairplay.dnssd_register_airplay.restype = c_int |
| 52 | libshairplay.dnssd_register_airplay.argtypes = [c_void_p, c_char_p, c_ushort, POINTER(c_char), c_int] |
| 53 | libshairplay.dnssd_unregister_raop.restype = None |
| 54 | libshairplay.dnssd_unregister_raop.argtypes = [c_void_p] |
| 55 | libshairplay.dnssd_unregister_airplay.restype = None |
| 56 | libshairplay.dnssd_unregister_airplay.argtypes = [c_void_p] |
| 57 | libshairplay.dnssd_destroy.restype = None |
| 58 | libshairplay.dnssd_destroy.argtypes = [c_void_p] |
| 59 | |
| 60 | # Initialize raop related functions |
| 61 | libshairplay.raop_init.restype = c_void_p |
| 62 | libshairplay.raop_init.argtypes = [POINTER(RaopNativeCallbacks), c_char_p] |
| 63 | libshairplay.raop_is_running.restype = c_int |
| 64 | libshairplay.raop_is_running.argtypes = [c_void_p] |
| 65 | libshairplay.raop_start.restype = c_int |
| 66 | libshairplay.raop_start.argtypes = [c_void_p, POINTER(c_ushort), POINTER(c_char), c_int, c_char_p] |
| 67 | libshairplay.raop_stop.restype = None |
| 68 | libshairplay.raop_stop.argtypes = [c_void_p] |
| 69 | libshairplay.raop_destroy.restype = None |
| 70 | libshairplay.raop_destroy.argtypes = [c_void_p] |
| 71 | |
| 72 | def LoadShairplay(path): |
| 73 | if sys.maxsize < 2**32: |
| 74 | libname = "shairplay32" |
| 75 | else: |
| 76 | libname = "shairplay64" |
| 77 | |
| 78 | if platform.system() == "Windows": |
| 79 | libname = libname + ".dll" |
| 80 | elif platform.system() == "Darwin": |
| 81 | libname = "lib" + libname + ".dylib" |
| 82 | else: |
| 83 | libname = "lib" + libname + ".so" |
| 84 | |
| 85 | try: |
| 86 | fullpath = os.path.join(path, libname) |
| 87 | libshairplay = cdll.LoadLibrary(fullpath) |
| 88 | except: |
| 89 | raise RuntimeError("Couldn't load shairplay library " + libname) |
| 90 | |
| 91 | InitShairplay(libshairplay) |
| 92 | return libshairplay |
| 93 | |
| 94 | RSA_KEY = """ |
| 95 | -----BEGIN RSA PRIVATE KEY----- |
| 96 | MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt |
| 97 | wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U |
| 98 | wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf |
| 99 | /+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/ |
| 100 | UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW |
| 101 | BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa |
| 102 | LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5 |
| 103 | NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm |
| 104 | lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz |
| 105 | aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu |
| 106 | a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM |
| 107 | oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z |
| 108 | oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+ |
| 109 | k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL |
| 110 | AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA |
| 111 | cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf |
| 112 | 54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov |
| 113 | 17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc |
| 114 | 1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI |
| 115 | LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ |
| 116 | 2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY= |
| 117 | -----END RSA PRIVATE KEY----- |
| 118 | """ |
| 119 | |
| 120 | class RaopCallbacks: |
| 121 | def audio_init(self, bits, channels, samplerate): |
| 122 | raise NotImplementedError() |
| 123 | |
| 124 | def audio_set_volume(self, session, volume): |
| 125 | pass |
| 126 | |
| 127 | def audio_process(self, session, buffer): |
| 128 | raise NotImplementedError() |
| 129 | |
| 130 | def audio_flush(self, session): |
| 131 | pass |
| 132 | |
| 133 | def audio_destroy(self, session): |
| 134 | pass |
| 135 | |
| 136 | class RaopService: |
| 137 | def audio_init_cb(self, cls, bits, channels, samplerate): |
| 138 | session = self.callbacks.audio_init(bits, channels, samplerate) |
| 139 | self.sessions.append(session) |
| 140 | return session |
| 141 | |
| 142 | def audio_set_volume_cb(self, cls, sessionptr, volume): |
| 143 | session = cast(sessionptr, py_object).value |
| 144 | self.callbacks.audio_set_volume(session, volume) |
| 145 | |
| 146 | def audio_process_cb(self, cls, sessionptr, buffer, buflen): |
| 147 | session = cast(sessionptr, py_object).value |
| 148 | strbuffer = string_at(buffer, buflen) |
| 149 | self.callbacks.audio_process(session, strbuffer) |
| 150 | |
| 151 | def audio_flush_cb(self, cls, sessionptr): |
| 152 | session = cast(sessionptr, py_object).value |
| 153 | self.callbacks.audio_flush(session) |
| 154 | |
| 155 | def audio_destroy_cb(self, cls, sessionptr): |
| 156 | session = cast(sessionptr, py_object).value |
| 157 | self.callbacks.audio_destroy(session) |
| 158 | if session in self.sessions: |
| 159 | self.sessions.remove(session) |
| 160 | |
| 161 | |
| 162 | def __init__(self, libshairplay, callbacks): |
| 163 | self.libshairplay = libshairplay |
| 164 | self.callbacks = callbacks |
| 165 | self.sessions = [] |
| 166 | self.instance = None |
| 167 | |
| 168 | # We need to hold a reference to native_callbacks |
| 169 | self.native_callbacks = RaopNativeCallbacks() |
| 170 | self.native_callbacks.audio_init = audio_init_prototype(self.audio_init_cb) |
| 171 | self.native_callbacks.audio_set_volume = audio_set_volume_prototype(self.audio_set_volume_cb) |
| 172 | self.native_callbacks.audio_process = audio_process_prototype(self.audio_process_cb) |
| 173 | self.native_callbacks.audio_flush = audio_flush_prototype(self.audio_flush_cb) |
| 174 | self.native_callbacks.audio_destroy = audio_destroy_prototype(self.audio_destroy_cb) |
| 175 | |
| 176 | # Initialize the raop instance with our callbacks |
| 177 | self.instance = self.libshairplay.raop_init(pointer(self.native_callbacks), RSA_KEY) |
| 178 | if self.instance == None: |
| 179 | raise RuntimeError("Initializing library failed") |
| 180 | |
| 181 | def __del__(self): |
| 182 | if self.instance != None: |
| 183 | self.libshairplay.raop_destroy(self.instance) |
| 184 | self.instance = None |
| 185 | |
| 186 | def is_running(self): |
| 187 | if self.libshairplay.raop_is_running(self.instance): |
| 188 | return True |
| 189 | else: |
| 190 | return False |
| 191 | |
| 192 | def start(self, port, hwaddrstr, password=None): |
| 193 | port = c_ushort(port) |
| 194 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) |
| 195 | |
| 196 | ret = self.libshairplay.raop_start(self.instance, pointer(port), hwaddr, c_int(len(hwaddr)), password) |
| 197 | if ret < 0: |
| 198 | raise RuntimeError("Starting RAOP instance failed") |
| 199 | return port.value |
| 200 | |
| 201 | def stop(self): |
| 202 | self.libshairplay.raop_stop(self.instance) |
| 203 | |
| 204 | class DnssdService: |
| 205 | def __init__(self, libshairplay): |
| 206 | self.libshairplay = libshairplay |
| 207 | self.instance = None |
| 208 | |
| 209 | error = c_int(0) |
| 210 | |
| 211 | self.instance = self.libshairplay.dnssd_init(pointer(error)) |
| 212 | if self.instance == None: |
| 213 | raise RuntimeError("Initializing library failed: " + str(error.value)) |
| 214 | |
| 215 | def __del__(self): |
| 216 | if self.instance != None: |
| 217 | self.libshairplay.dnssd_destroy(self.instance) |
| 218 | self.instance = None |
| 219 | |
| 220 | def register_raop(self, name, port, hwaddrstr, password=False): |
| 221 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) |
| 222 | use_pw = c_int(0) |
| 223 | if password: |
| 224 | use_pw = c_int(1) |
| 225 | self.libshairplay.dnssd_register_raop(self.instance, name, c_ushort(port), hwaddr, len(hwaddr), use_pw) |
| 226 | |
| 227 | def unregister_raop(self): |
| 228 | self.libshairplay.dnssd_unregister_raop(self.instance) |
| 229 | |
| 230 | def register_airplay(self, name, port, hwaddrstr): |
| 231 | hwaddr = create_string_buffer(hwaddrstr, len(hwaddrstr)) |
| 232 | self.libshairplay.dnssd_register_airplay(self.instance, name, c_ushort(port), hwaddr, len(hwaddr)) |
| 233 | |
| 234 | def unregister_airplay(self): |
| 235 | self.libshairplay.dnssd_unregister_airplay(self.instance) |
| 236 | |