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