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