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