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