| 1 | #include "mythverbose.h" |
| 2 | #include "util.h" |
| 3 | #include "audiopulsehandler.h" |
| 4 | |
| 5 | #define LOC QString("Pulse: ") |
| 6 | #define ERR QString("Pulse err: ") |
| 7 | #define WAR QString("Pulse Warning: ") |
| 8 | |
| 9 | #define IS_READY(arg) ((PA_CONTEXT_READY == arg) || \ |
| 10 | (PA_CONTEXT_FAILED == arg) || \ |
| 11 | (PA_CONTEXT_TERMINATED == arg)) |
| 12 | |
| 13 | static QString state_to_string(pa_context_state state) |
| 14 | { |
| 15 | QString ret = "Unknown"; |
| 16 | switch (state) |
| 17 | { |
| 18 | case PA_CONTEXT_UNCONNECTED: ret = "Unconnected"; break; |
| 19 | case PA_CONTEXT_CONNECTING: ret = "Connecting"; break; |
| 20 | case PA_CONTEXT_AUTHORIZING: ret = "Authorizing"; break; |
| 21 | case PA_CONTEXT_SETTING_NAME: ret = "Setting name"; break; |
| 22 | case PA_CONTEXT_READY: ret = "Ready!"; break; |
| 23 | case PA_CONTEXT_FAILED: ret = "Failed"; break; |
| 24 | case PA_CONTEXT_TERMINATED: ret = "Terminated"; break; |
| 25 | } |
| 26 | return ret; |
| 27 | } |
| 28 | |
| 29 | PulseHandler* PulseHandler::g_pulseHandler = NULL; |
| 30 | bool PulseHandler::g_pulseHandlerActive = false; |
| 31 | |
| 32 | bool PulseHandler::Suspend(bool suspend) |
| 33 | { |
| 34 | // global lock around all access to our global singleton |
| 35 | static QMutex global_lock; |
| 36 | QMutexLocker locker(&global_lock); |
| 37 | |
| 38 | // do nothing if PulseAudio is not currently running |
| 39 | if (!IsPulseAudioRunning()) |
| 40 | { |
| 41 | VERBOSE(VB_AUDIO, LOC + "PulseAudio not running"); |
| 42 | return false; |
| 43 | } |
| 44 | |
| 45 | // make sure any pre-existing handler is still valid |
| 46 | if (g_pulseHandler && !g_pulseHandler->Valid()) |
| 47 | { |
| 48 | VERBOSE(VB_IMPORTANT, WAR + "PulseHandler invalidated. Deleting."); |
| 49 | delete g_pulseHandler; |
| 50 | g_pulseHandler = NULL; |
| 51 | } |
| 52 | |
| 53 | // create our handler |
| 54 | if (!g_pulseHandler) |
| 55 | { |
| 56 | PulseHandler* handler = new PulseHandler(); |
| 57 | if (handler) |
| 58 | { |
| 59 | VERBOSE(VB_AUDIO, LOC + "Created PulseHandler object"); |
| 60 | g_pulseHandler = handler; |
| 61 | } |
| 62 | else |
| 63 | { |
| 64 | VERBOSE(VB_IMPORTANT, ERR + "Failed to create PulseHandler object"); |
| 65 | return false; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | bool result; |
| 70 | // enable processing of incoming callbacks |
| 71 | g_pulseHandlerActive = true; |
| 72 | result = g_pulseHandler->SuspendInternal(suspend); |
| 73 | // disable processing of incoming callbacks in case we delete/recreate our |
| 74 | // instance due to a termination or other failure |
| 75 | g_pulseHandlerActive = false; |
| 76 | return result; |
| 77 | } |
| 78 | |
| 79 | static void StatusCallback(pa_context *ctx, void *userdata) |
| 80 | { |
| 81 | // ignore any status updates while we're inactive, we can update |
| 82 | // directly as needed |
| 83 | if (!ctx || !PulseHandler::g_pulseHandlerActive) |
| 84 | return; |
| 85 | |
| 86 | // validate the callback |
| 87 | PulseHandler *handler = static_cast<PulseHandler*>(userdata); |
| 88 | if (!handler) |
| 89 | { |
| 90 | VERBOSE(VB_IMPORTANT, ERR + "Callback: no handler."); |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | if (handler->m_ctx != ctx) |
| 95 | { |
| 96 | VERBOSE(VB_IMPORTANT, ERR + "Callback: handler/context mismatch."); |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | if (handler != PulseHandler::g_pulseHandler) |
| 101 | { |
| 102 | VERBOSE(VB_IMPORTANT, ERR + |
| 103 | "Callback: returned handler is not the global handler."); |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | // update our status |
| 108 | pa_context_state state = pa_context_get_state(ctx); |
| 109 | VERBOSE(VB_AUDIO, LOC + QString("Callback: State changed %1->%2") |
| 110 | .arg(state_to_string(handler->m_ctx_state)) |
| 111 | .arg(state_to_string(state))); |
| 112 | handler->m_ctx_state = state; |
| 113 | } |
| 114 | |
| 115 | static void OperationCallback(pa_context *ctx, int success, void *userdata) |
| 116 | { |
| 117 | if (!ctx) |
| 118 | return; |
| 119 | |
| 120 | // ignore late updates but flag them as they may be an issue |
| 121 | if (!PulseHandler::g_pulseHandlerActive) |
| 122 | { |
| 123 | VERBOSE(VB_IMPORTANT, WAR + "Received a late/unexpected operation " |
| 124 | "callback. Ignoring."); |
| 125 | return; |
| 126 | } |
| 127 | |
| 128 | // validate the callback |
| 129 | PulseHandler *handler = static_cast<PulseHandler*>(userdata); |
| 130 | if (!handler) |
| 131 | { |
| 132 | VERBOSE(VB_IMPORTANT, ERR + "Operation: no handler."); |
| 133 | return; |
| 134 | } |
| 135 | |
| 136 | if (handler->m_ctx != ctx) |
| 137 | { |
| 138 | VERBOSE(VB_IMPORTANT, ERR + "Operation: handler/context mismatch."); |
| 139 | return; |
| 140 | } |
| 141 | |
| 142 | if (handler != PulseHandler::g_pulseHandler) |
| 143 | { |
| 144 | VERBOSE(VB_IMPORTANT, ERR + |
| 145 | "Operation: returned handler is not the global handler."); |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | // update the context |
| 150 | handler->m_pending_operations--; |
| 151 | VERBOSE(VB_AUDIO, LOC + QString("Operation: success %1 remaining %2") |
| 152 | .arg(success).arg(handler->m_pending_operations)); |
| 153 | } |
| 154 | |
| 155 | PulseHandler::PulseHandler(void) |
| 156 | : m_ctx_state(PA_CONTEXT_UNCONNECTED), m_ctx(NULL), m_pending_operations(0), |
| 157 | m_loop(NULL), m_initialised(false), m_valid(false), |
| 158 | m_thread(NULL) |
| 159 | { |
| 160 | } |
| 161 | |
| 162 | PulseHandler::~PulseHandler(void) |
| 163 | { |
| 164 | // TODO - do we need to drain the context?? |
| 165 | |
| 166 | VERBOSE(VB_AUDIO, LOC + "Destroying PulseAudio handler"); |
| 167 | |
| 168 | // is this correct? |
| 169 | if (m_ctx) |
| 170 | { |
| 171 | pa_context_disconnect(m_ctx); |
| 172 | pa_context_unref(m_ctx); |
| 173 | } |
| 174 | |
| 175 | if (m_loop) |
| 176 | { |
| 177 | pa_signal_done(); |
| 178 | pa_mainloop_free(m_loop); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | bool PulseHandler::Valid(void) |
| 183 | { |
| 184 | if (m_initialised && m_valid) |
| 185 | { |
| 186 | m_ctx_state = pa_context_get_state(m_ctx); |
| 187 | return PA_CONTEXT_READY == m_ctx_state; |
| 188 | } |
| 189 | return false; |
| 190 | } |
| 191 | |
| 192 | bool PulseHandler::Init(void) |
| 193 | { |
| 194 | if (m_initialised) |
| 195 | return m_valid; |
| 196 | m_initialised = true; |
| 197 | |
| 198 | // Initialse our connection to the server |
| 199 | m_loop = pa_mainloop_new(); |
| 200 | if (!m_loop) |
| 201 | { |
| 202 | VERBOSE(VB_IMPORTANT, ERR + "Failed to get PulseAudio mainloop"); |
| 203 | return m_valid; |
| 204 | } |
| 205 | |
| 206 | pa_mainloop_api *api = pa_mainloop_get_api(m_loop); |
| 207 | if (!api) |
| 208 | { |
| 209 | VERBOSE(VB_IMPORTANT, ERR + "Failed to get PulseAudio api"); |
| 210 | return m_valid; |
| 211 | } |
| 212 | |
| 213 | if (pa_signal_init(api) != 0) |
| 214 | { |
| 215 | VERBOSE(VB_IMPORTANT, ERR + "Failed to initialise signaling"); |
| 216 | return m_valid; |
| 217 | } |
| 218 | |
| 219 | const char *client = "mythtv"; |
| 220 | m_ctx = pa_context_new(api, client); |
| 221 | if (!m_ctx) |
| 222 | { |
| 223 | VERBOSE(VB_IMPORTANT, ERR + "Failed to create context"); |
| 224 | return m_valid; |
| 225 | } |
| 226 | |
| 227 | // remember which thread created this object for later sanity debugging |
| 228 | m_thread = QThread::currentThread(); |
| 229 | |
| 230 | // we set the callback, connect and then run the main loop 'by hand' |
| 231 | // until we've successfully connected (or not) |
| 232 | pa_context_set_state_callback(m_ctx, StatusCallback, this); |
| 233 | pa_context_connect(m_ctx, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL); |
| 234 | int ret = 0; |
| 235 | int tries = 0; |
| 236 | while ((tries++ < 100) && !IS_READY(m_ctx_state)) |
| 237 | { |
| 238 | pa_mainloop_iterate(m_loop, 0, &ret); |
| 239 | usleep(10000); |
| 240 | } |
| 241 | |
| 242 | if (PA_CONTEXT_READY != m_ctx_state) |
| 243 | { |
| 244 | VERBOSE(VB_IMPORTANT, ERR + "Context not ready after 1000ms"); |
| 245 | return m_valid; |
| 246 | } |
| 247 | |
| 248 | VERBOSE(VB_AUDIO, LOC + "Initialised handler"); |
| 249 | m_valid = true; |
| 250 | return m_valid; |
| 251 | } |
| 252 | |
| 253 | bool PulseHandler::SuspendInternal(bool suspend) |
| 254 | { |
| 255 | // set everything up... |
| 256 | if (!Init()) |
| 257 | return false; |
| 258 | |
| 259 | // just in case it all goes pete tong |
| 260 | if (QThread::currentThread() != m_thread) |
| 261 | VERBOSE(VB_AUDIO, WAR + "PulseHandler called from a different thread"); |
| 262 | |
| 263 | QString action = suspend ? "suspend" : "resume"; |
| 264 | // don't try and suspend a networked server (why not??) |
| 265 | if (!pa_context_is_local(m_ctx)) |
| 266 | { |
| 267 | VERBOSE(VB_IMPORTANT, ERR + "Sound server is remote. Cannot " + action); |
| 268 | return false; |
| 269 | } |
| 270 | |
| 271 | // create and dispatch 2 operations to suspend or resume all current sinks |
| 272 | // and all current sources |
| 273 | m_pending_operations = 2; |
| 274 | pa_operation *operation_sink = |
| 275 | pa_context_suspend_sink_by_index( |
| 276 | m_ctx, PA_INVALID_INDEX, suspend, OperationCallback, this); |
| 277 | pa_operation_unref(operation_sink); |
| 278 | |
| 279 | pa_operation *operation_source = |
| 280 | pa_context_suspend_source_by_index( |
| 281 | m_ctx, PA_INVALID_INDEX, suspend, OperationCallback, this); |
| 282 | pa_operation_unref(operation_source); |
| 283 | |
| 284 | // run the loop manually and wait for the callbacks |
| 285 | int count = 0; |
| 286 | int ret = 0; |
| 287 | while (m_pending_operations && count++ < 100) |
| 288 | { |
| 289 | pa_mainloop_iterate(m_loop, 0, &ret); |
| 290 | usleep(10000); |
| 291 | } |
| 292 | |
| 293 | // a failure isn't necessarily disastrous |
| 294 | if (m_pending_operations) |
| 295 | { |
| 296 | m_pending_operations = 0; |
| 297 | VERBOSE(VB_IMPORTANT, ERR + "Failed to " + action); |
| 298 | return false; |
| 299 | } |
| 300 | |
| 301 | // rejoice |
| 302 | VERBOSE(VB_GENERAL, LOC + "PaulseAudio " + action + " OK"); |
| 303 | return true; |
| 304 | } |