1 /**
2  * \file vlcpulse.c
3  * \brief PulseAudio support library for LibVLC plugins
4  */
5 /*****************************************************************************
6  * Copyright (C) 2009-2011 Rémi Denis-Courmont
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 2.1 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public License
19  * along with this program; if not, write to the Free Software Foundation,
20  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
21  *****************************************************************************/
22 
23 #ifdef HAVE_CONFIG_H
24 # include "config.h"
25 #endif
26 
27 #include <vlc_common.h>
28 #include <pulse/pulseaudio.h>
29 
30 #include "audio_output/vlcpulse.h"
31 #include <assert.h>
32 #include <stdlib.h>
33 #include <locale.h>
34 #include <unistd.h>
35 #include <pwd.h>
36 
37 const char vlc_module_name[] = "vlcpulse";
38 
39 #undef vlc_pa_error
vlc_pa_error(vlc_object_t * obj,const char * msg,pa_context * ctx)40 void vlc_pa_error (vlc_object_t *obj, const char *msg, pa_context *ctx)
41 {
42     msg_Err (obj, "%s: %s", msg, pa_strerror (pa_context_errno (ctx)));
43 }
44 
context_state_cb(pa_context * ctx,void * userdata)45 static void context_state_cb (pa_context *ctx, void *userdata)
46 {
47     pa_threaded_mainloop *mainloop = userdata;
48 
49     switch (pa_context_get_state(ctx))
50     {
51         case PA_CONTEXT_READY:
52         case PA_CONTEXT_FAILED:
53         case PA_CONTEXT_TERMINATED:
54             pa_threaded_mainloop_signal (mainloop, 0);
55         default:
56             break;
57     }
58 }
59 
context_wait(pa_context * ctx,pa_threaded_mainloop * mainloop)60 static bool context_wait (pa_context *ctx, pa_threaded_mainloop *mainloop)
61 {
62     pa_context_state_t state;
63 
64     while ((state = pa_context_get_state (ctx)) != PA_CONTEXT_READY)
65     {
66         if (state == PA_CONTEXT_FAILED || state == PA_CONTEXT_TERMINATED)
67             return -1;
68         pa_threaded_mainloop_wait (mainloop);
69     }
70     return 0;
71 }
72 
context_event_cb(pa_context * c,const char * name,pa_proplist * pl,void * userdata)73 static void context_event_cb(pa_context *c, const char *name, pa_proplist *pl,
74                              void *userdata)
75 {
76     vlc_object_t *obj = userdata;
77 
78     msg_Warn (obj, "unhandled context event \"%s\"", name);
79     (void) c;
80     (void) pl;
81 }
82 
83 /**
84  * Initializes the PulseAudio main loop and connects to the PulseAudio server.
85  * @return a PulseAudio context on success, or NULL on error
86  */
vlc_pa_connect(vlc_object_t * obj,pa_threaded_mainloop ** mlp)87 pa_context *vlc_pa_connect (vlc_object_t *obj, pa_threaded_mainloop **mlp)
88 {
89     msg_Dbg (obj, "using library version %s", pa_get_library_version ());
90     msg_Dbg (obj, " (compiled with version %s, protocol %u)",
91              pa_get_headers_version (), PA_PROTOCOL_VERSION);
92 
93     /* Initialize main loop */
94     pa_threaded_mainloop *mainloop = pa_threaded_mainloop_new ();
95     if (unlikely(mainloop == NULL))
96         return NULL;
97 
98     if (pa_threaded_mainloop_start (mainloop) < 0)
99     {
100         pa_threaded_mainloop_free (mainloop);
101         return NULL;
102     }
103 
104     /* Fill in context (client) properties */
105     char *ua = var_InheritString (obj, "user-agent");
106     pa_proplist *props = pa_proplist_new ();
107     if (likely(props != NULL))
108     {
109         char *str;
110 
111         if (ua != NULL)
112             pa_proplist_sets (props, PA_PROP_APPLICATION_NAME, ua);
113 
114         str = var_InheritString (obj, "app-id");
115         if (str != NULL)
116         {
117             pa_proplist_sets (props, PA_PROP_APPLICATION_ID, str);
118             free (str);
119         }
120         str = var_InheritString (obj, "app-version");
121         if (str != NULL)
122         {
123             pa_proplist_sets (props, PA_PROP_APPLICATION_VERSION, str);
124             free (str);
125         }
126         str = var_InheritString (obj, "app-icon-name");
127         if (str != NULL)
128         {
129             pa_proplist_sets (props, PA_PROP_APPLICATION_ICON_NAME, str);
130             free (str);
131         }
132         //pa_proplist_sets (props, PA_PROP_APPLICATION_LANGUAGE, _("C"));
133         pa_proplist_sets (props, PA_PROP_APPLICATION_LANGUAGE,
134                           setlocale (LC_MESSAGES, NULL));
135 
136         pa_proplist_setf (props, PA_PROP_APPLICATION_PROCESS_ID, "%lu",
137                           (unsigned long) getpid ());
138         //pa_proplist_sets (props, PA_PROP_APPLICATION_PROCESS_BINARY,
139         //                  PACKAGE_NAME);
140 
141         for (size_t max = sysconf (_SC_GETPW_R_SIZE_MAX), len = max % 1024 + 1024;
142              len < max; len += 1024)
143         {
144             struct passwd pwbuf, *pw;
145             char buf[len];
146 
147             if (getpwuid_r (getuid (), &pwbuf, buf, sizeof (buf), &pw) == 0)
148             {
149                 if (pw != NULL)
150                     pa_proplist_sets (props, PA_PROP_APPLICATION_PROCESS_USER,
151                                       pw->pw_name);
152                 break;
153             }
154         }
155 
156         for (size_t max = sysconf (_SC_HOST_NAME_MAX), len = max % 1024 + 1024;
157              len < max; len += 1024)
158         {
159             char hostname[len];
160 
161             if (gethostname (hostname, sizeof (hostname)) == 0)
162             {
163                 pa_proplist_sets (props, PA_PROP_APPLICATION_PROCESS_HOST,
164                                   hostname);
165                 break;
166             }
167         }
168 
169         const char *session = getenv ("XDG_SESSION_COOKIE");
170         if (session != NULL)
171         {
172             pa_proplist_setf (props, PA_PROP_APPLICATION_PROCESS_MACHINE_ID,
173                               "%.32s", session); /* XXX: is this valid? */
174             pa_proplist_sets (props, PA_PROP_APPLICATION_PROCESS_SESSION_ID,
175                               session);
176         }
177     }
178 
179     /* Connect to PulseAudio daemon */
180     pa_context *ctx;
181     pa_mainloop_api *api;
182 
183     pa_threaded_mainloop_lock (mainloop);
184     api = pa_threaded_mainloop_get_api (mainloop);
185     ctx = pa_context_new_with_proplist (api, ua, props);
186     free (ua);
187     if (props != NULL)
188         pa_proplist_free (props);
189     if (unlikely(ctx == NULL))
190         goto fail;
191 
192     pa_context_set_state_callback (ctx, context_state_cb, mainloop);
193     pa_context_set_event_callback (ctx, context_event_cb, obj);
194     if (pa_context_connect (ctx, NULL, 0, NULL) < 0
195      || context_wait (ctx, mainloop))
196     {
197         vlc_pa_error (obj, "PulseAudio server connection failure", ctx);
198         pa_context_unref (ctx);
199         goto fail;
200     }
201     msg_Dbg (obj, "connected %s to %s as client #%"PRIu32,
202              pa_context_is_local (ctx) ? "locally" : "remotely",
203              pa_context_get_server (ctx), pa_context_get_index (ctx));
204     msg_Dbg (obj, "using protocol %"PRIu32", server protocol %"PRIu32,
205              pa_context_get_protocol_version (ctx),
206              pa_context_get_server_protocol_version (ctx));
207 
208     pa_threaded_mainloop_unlock (mainloop);
209     *mlp = mainloop;
210     return ctx;
211 
212 fail:
213     pa_threaded_mainloop_unlock (mainloop);
214     pa_threaded_mainloop_stop (mainloop);
215     pa_threaded_mainloop_free (mainloop);
216     return NULL;
217 }
218 
219 /**
220  * Closes a connection to PulseAudio.
221  */
vlc_pa_disconnect(vlc_object_t * obj,pa_context * ctx,pa_threaded_mainloop * mainloop)222 void vlc_pa_disconnect (vlc_object_t *obj, pa_context *ctx,
223                         pa_threaded_mainloop *mainloop)
224 {
225     pa_threaded_mainloop_lock (mainloop);
226     pa_context_disconnect (ctx);
227     pa_context_set_event_callback (ctx, NULL, NULL);
228     pa_context_set_state_callback (ctx, NULL, NULL);
229     pa_context_unref (ctx);
230     pa_threaded_mainloop_unlock (mainloop);
231 
232     pa_threaded_mainloop_stop (mainloop);
233     pa_threaded_mainloop_free (mainloop);
234     (void) obj;
235 }
236 
237 /**
238  * Frees a timer event.
239  * \note Timer events can be created with pa_context_rttime_new().
240  * \warning This function must be called from the mainloop,
241  * or with the mainloop lock held.
242  */
vlc_pa_rttime_free(pa_threaded_mainloop * mainloop,pa_time_event * e)243 void vlc_pa_rttime_free (pa_threaded_mainloop *mainloop, pa_time_event *e)
244 {
245     pa_mainloop_api *api = pa_threaded_mainloop_get_api (mainloop);
246 
247     api->time_free (e);
248 }
249 
250 #undef vlc_pa_get_latency
251 /**
252  * Gets latency of a PulseAudio stream.
253  * \return the latency or VLC_TS_INVALID on error.
254  */
vlc_pa_get_latency(vlc_object_t * obj,pa_context * ctx,pa_stream * s)255 mtime_t vlc_pa_get_latency(vlc_object_t *obj, pa_context *ctx, pa_stream *s)
256 {
257     /* NOTE: pa_stream_get_latency() will report 0 rather than negative latency
258      * when the write index of a playback stream is behind its read index.
259      * playback streams. So use the lower-level pa_stream_get_timing_info()
260      * directly to obtain the correct write index and convert it to a time,
261      * and compute the correct latency value by subtracting the stream (read)
262      * time.
263      *
264      * On the read side, pa_stream_get_time() is used instead of
265      * pa_stream_get_timing_info() for the sake of interpolation. */
266     const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
267     const pa_timing_info *ti = pa_stream_get_timing_info(s);
268 
269     if (ti == NULL) {
270         msg_Dbg(obj, "no timing infos");
271         return VLC_TS_INVALID;
272     }
273 
274     if (ti->write_index_corrupt) {
275         msg_Dbg(obj, "write index corrupt");
276         return VLC_TS_INVALID;
277     }
278 
279     pa_usec_t wt = pa_bytes_to_usec((uint64_t)ti->write_index, ss);
280     pa_usec_t rt;
281 
282     if (pa_stream_get_time(s, &rt)) {
283         if (pa_context_errno(ctx) != PA_ERR_NODATA)
284             vlc_pa_error(obj, "unknown time", ctx);
285         return VLC_TS_INVALID;
286     }
287 
288     union { uint64_t u; int64_t s; } d;
289     d.u = wt - rt;
290     return d.s; /* non-overflowing unsigned to signed conversion */
291 }
292