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