MythTV  master
audiopulsehandler.cpp
Go to the documentation of this file.
1 #include <QMutexLocker>
2 #include <QString>
3 #include <QMutex>
4 #include <QElapsedTimer>
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 s_globalLock;
42  QMutexLocker locker(&s_globalLock);
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 QElapsedTimer s_time;
58  static auto s_ePulseAction = PulseAction(-1);
59 
60  // Use the last result of IsPulseAudioRunning if within time
61  if (s_time.isValid() && !s_time.hasExpired(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  auto* 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  // enable processing of incoming callbacks
111  g_pulseHandlerActive = true;
113  // disable processing of incoming callbacks in case we delete/recreate our
114  // instance due to a termination or other failure
115  g_pulseHandlerActive = false;
116  s_ePulseAction = action;
117  return result;
118 }
119 
120 static void StatusCallback(pa_context *ctx, void *userdata)
121 {
122  // ignore any status updates while we're inactive, we can update
123  // directly as needed
125  return;
126 
127  // validate the callback
128  auto *handler = static_cast<PulseHandler*>(userdata);
129  if (!handler)
130  {
131  LOG(VB_GENERAL, LOG_ERR, LOC + "Callback: no handler.");
132  return;
133  }
134 
135  if (handler->m_ctx != ctx)
136  {
137  LOG(VB_GENERAL, LOG_ERR, LOC + "Callback: handler/context mismatch.");
138  return;
139  }
140 
141  if (handler != PulseHandler::g_pulseHandler)
142  {
143  LOG(VB_GENERAL, LOG_ERR,
144  "Callback: returned handler is not the global handler.");
145  return;
146  }
147 
148  // update our status
149  pa_context_state state = pa_context_get_state(ctx);
150  LOG(VB_AUDIO, LOG_INFO, LOC + QString("Callback: State changed %1->%2")
151  .arg(state_to_string(handler->m_ctxState))
152  .arg(state_to_string(state)));
153  handler->m_ctxState = state;
154 }
155 
156 static void OperationCallback(pa_context *ctx, int success, void *userdata)
157 {
158  if (!ctx)
159  return;
160 
161  // ignore late updates but flag them as they may be an issue
163  {
164  LOG(VB_GENERAL, LOG_WARNING, LOC +
165  "Received a late/unexpected operation callback. Ignoring.");
166  return;
167  }
168 
169  // validate the callback
170  auto *handler = static_cast<PulseHandler*>(userdata);
171  if (!handler)
172  {
173  LOG(VB_GENERAL, LOG_ERR, LOC + "Operation: no handler.");
174  return;
175  }
176 
177  if (handler->m_ctx != ctx)
178  {
179  LOG(VB_GENERAL, LOG_ERR, LOC + "Operation: handler/context mismatch.");
180  return;
181  }
182 
183  if (handler != PulseHandler::g_pulseHandler)
184  {
185  LOG(VB_GENERAL, LOG_ERR, LOC +
186  "Operation: returned handler is not the global handler.");
187  return;
188  }
189 
190  // update the context
191  handler->m_pendingOperations--;
192  LOG(VB_AUDIO, LOG_INFO, LOC + QString("Operation: success %1 remaining %2")
193  .arg(success).arg(handler->m_pendingOperations));
194 }
195 
197 {
198  // TODO - do we need to drain the context??
199 
200  LOG(VB_AUDIO, LOG_INFO, LOC + "Destroying PulseAudio handler");
201 
202  // is this correct?
203  if (m_ctx)
204  {
205  pa_context_disconnect(m_ctx);
206  pa_context_unref(m_ctx);
207  }
208 
209  if (m_loop)
210  {
211  pa_signal_done();
212  pa_mainloop_free(m_loop);
213  }
214 }
215 
217 {
218  if (m_initialised && m_valid)
219  {
220  m_ctxState = pa_context_get_state(m_ctx);
221  return PA_CONTEXT_READY == m_ctxState;
222  }
223  return false;
224 }
225 
227 {
228  if (m_initialised)
229  return m_valid;
230  m_initialised = true;
231 
232  // Initialse our connection to the server
233  m_loop = pa_mainloop_new();
234  if (!m_loop)
235  {
236  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to get PulseAudio mainloop");
237  return m_valid;
238  }
239 
240  pa_mainloop_api *api = pa_mainloop_get_api(m_loop);
241  if (!api)
242  {
243  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to get PulseAudio api");
244  return m_valid;
245  }
246 
247  if (pa_signal_init(api) != 0)
248  {
249  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to initialise signaling");
250  return m_valid;
251  }
252 
253  const char *client = "mythtv";
254  m_ctx = pa_context_new(api, client);
255  if (!m_ctx)
256  {
257  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create context");
258  return m_valid;
259  }
260 
261  // remember which thread created this object for later sanity debugging
262  m_thread = QThread::currentThread();
263 
264  // we set the callback, connect and then run the main loop 'by hand'
265  // until we've successfully connected (or not)
266  pa_context_set_state_callback(m_ctx, StatusCallback, this);
267  pa_context_connect(m_ctx, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr);
268  int ret = 0;
269  int tries = 0;
270  while ((tries++ < 100) && !IS_READY(m_ctxState))
271  {
272  pa_mainloop_iterate(m_loop, 0, &ret);
273  usleep(10000);
274  }
275 
276  if (PA_CONTEXT_READY != m_ctxState)
277  {
278  LOG(VB_GENERAL, LOG_ERR, LOC + "Context not ready after 1000ms");
279  return m_valid;
280  }
281 
282  LOG(VB_AUDIO, LOG_INFO, LOC + "Initialised handler");
283  m_valid = true;
284  return m_valid;
285 }
286 
288 {
289  // set everything up...
290  if (!Init())
291  return false;
292 
293  // just in case it all goes pete tong
295  LOG(VB_AUDIO, LOG_WARNING, LOC +
296  "PulseHandler called from a different thread");
297 
298  QString action = suspend ? "suspend" : "resume";
299  // don't bother to suspend a networked server
300  if (!pa_context_is_local(m_ctx))
301  {
302  LOG(VB_GENERAL, LOG_ERR, LOC +
303  "PulseAudio server is remote. No need to " + action);
304  return false;
305  }
306 
307  // create and dispatch 2 operations to suspend or resume all current sinks
308  // and all current sources
310  pa_operation *operation_sink =
311  pa_context_suspend_sink_by_index(
312  m_ctx, PA_INVALID_INDEX, static_cast<int>(suspend), OperationCallback, this);
313  pa_operation_unref(operation_sink);
314 
315  pa_operation *operation_source =
316  pa_context_suspend_source_by_index(
317  m_ctx, PA_INVALID_INDEX, static_cast<int>(suspend), OperationCallback, this);
318  pa_operation_unref(operation_source);
319 
320  // run the loop manually and wait for the callbacks
321  int count = 0;
322  int ret = 0;
323  while (m_pendingOperations && count++ < 100)
324  {
325  pa_mainloop_iterate(m_loop, 0, &ret);
326  usleep(10000);
327  }
328 
329  // a failure isn't necessarily disastrous
331  {
333  LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to " + action);
334  return false;
335  }
336 
337  // rejoice
338  LOG(VB_GENERAL, LOG_INFO, LOC + "PulseAudio " + action + " OK");
339  return true;
340 }
pa_context_state m_ctxState
static void OperationCallback(pa_context *ctx, int success, void *userdata)
void(*)(int, void *) StatusCallback
Definition: mythplayer.h:57
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 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)
static QString state_to_string(pa_context_state state)
#define IS_READY(arg)
static PulseHandler * g_pulseHandler
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23