meta-pipewire: add recipe to build the bluez-alsa-pipewire gstreamer helper
[AGL/meta-agl-devel.git] / meta-pipewire / recipes-connectivity / bluez-alsa / bluez-alsa / 0001-utils-add-a-gstreamer-helper-application-for-interco.patch
1 From 33555a493af67f3acc2129764a1b093aec6254d8 Mon Sep 17 00:00:00 2001
2 From: George Kiagiadakis <george.kiagiadakis@collabora.com>
3 Date: Fri, 4 Oct 2019 20:51:24 +0300
4 Subject: [PATCH] utils: add a gstreamer helper application for interconnection
5  with pipewire
6
7 Unfortunately, the bluez-alsa PCM plugin does not work correctly
8 when it is used through pipewire (or gstreamer, or anywhere really...).
9
10 Thanfully, the bluez-alsa PCM plugin is only a simple client that
11 reads/writes on a file descriptor that was opened by bluealsa.
12 This allows us to use bluealsa without the PCM plugin, just like it
13 is done in the aplay.c util.
14
15 This one uses GStreamer to implement the plumbing between pipewire
16 and the file descriptor. On the reading side we are also doing some
17 tricks to ensure a smooth stream, which is not the case for the
18 stream that is coming out of bluealsa.
19
20 This helper is implemented as a patch to bluez-alsa so that it can
21 use its internal private API. In the future this needs some re-thinking.
22
23 Upstream-Status: Inappropriate
24 ---
25  configure.ac       |   7 +
26  utils/Makefile.am  |  20 +++
27  utils/gst-helper.c | 379 +++++++++++++++++++++++++++++++++++++++++++++
28  3 files changed, 406 insertions(+)
29  create mode 100644 utils/gst-helper.c
30
31 diff --git a/configure.ac b/configure.ac
32 index 4825afa..9125871 100644
33 --- a/configure.ac
34 +++ b/configure.ac
35 @@ -141,6 +141,13 @@ AM_COND_IF([ENABLE_HCITOP], [
36         PKG_CHECK_MODULES([NCURSES], [ncurses])
37  ])
38  
39 +AC_ARG_ENABLE([gsthelper],
40 +       [AS_HELP_STRING([--enable-gsthelper], [enable building of gsthelper tool])])
41 +AM_CONDITIONAL([ENABLE_GSTHELPER], [test "x$enable_gsthelper" = "xyes"])
42 +AM_COND_IF([ENABLE_GSTHELPER], [
43 +       PKG_CHECK_MODULES([GST], [gstreamer-1.0 glib-2.0])
44 +])
45 +
46  AC_ARG_ENABLE([test],
47         [AS_HELP_STRING([--enable-test], [enable unit test])])
48  AM_CONDITIONAL([ENABLE_TEST], [test "x$enable_test" = "xyes"])
49 diff --git a/utils/Makefile.am b/utils/Makefile.am
50 index 9057f2c..9790474 100644
51 --- a/utils/Makefile.am
52 +++ b/utils/Makefile.am
53 @@ -47,3 +47,23 @@ hcitop_LDADD = \
54         @LIBBSD_LIBS@ \
55         @NCURSES_LIBS@
56  endif
57 +
58 +if ENABLE_GSTHELPER
59 +bin_PROGRAMS += bluealsa-gst-helper
60 +bluealsa_gst_helper_SOURCES = \
61 +       ../src/shared/dbus-client.c \
62 +       ../src/shared/ffb.c \
63 +       ../src/shared/log.c \
64 +       gst-helper.c
65 +bluealsa_gst_helper_CFLAGS = \
66 +       -I$(top_srcdir)/src \
67 +       @ALSA_CFLAGS@ \
68 +       @BLUEZ_CFLAGS@ \
69 +       @DBUS1_CFLAGS@ \
70 +       @GST_CFLAGS@
71 +bluealsa_gst_helper_LDADD = \
72 +       @ALSA_LIBS@ \
73 +       @BLUEZ_LIBS@ \
74 +       @DBUS1_LIBS@ \
75 +       @GST_LIBS@
76 +endif
77 diff --git a/utils/gst-helper.c b/utils/gst-helper.c
78 new file mode 100644
79 index 0000000..1b021ee
80 --- /dev/null
81 +++ b/utils/gst-helper.c
82 @@ -0,0 +1,379 @@
83 +/* Bluez-Alsa PipeWire integration GStreamer helper
84 + *
85 + * Copyright © 2016-2019 Arkadiusz Bokowy
86 + * Copyright © 2019 Collabora Ltd.
87 + *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
88 + *
89 + * SPDX-License-Identifier: MIT
90 + */
91 +
92 +#if HAVE_CONFIG_H
93 +# include <config.h>
94 +#endif
95 +
96 +#include <errno.h>
97 +#include <getopt.h>
98 +#include <poll.h>
99 +#include <pthread.h>
100 +#include <signal.h>
101 +#include <stdbool.h>
102 +#include <stdint.h>
103 +#include <stdio.h>
104 +#include <stdlib.h>
105 +#include <string.h>
106 +#include <unistd.h>
107 +
108 +#include <bluetooth/bluetooth.h>
109 +#include <dbus/dbus.h>
110 +#include <gst/gst.h>
111 +
112 +#include "shared/dbus-client.h"
113 +#include "shared/defs.h"
114 +#include "shared/ffb.h"
115 +#include "shared/log.h"
116 +
117 +struct worker {
118 +       /* used BlueALSA PCM device */
119 +       struct ba_pcm ba_pcm;
120 +       /* file descriptor of PCM FIFO */
121 +       int ba_pcm_fd;
122 +       /* file descriptor of PCM control */
123 +       int ba_pcm_ctrl_fd;
124 +       /* the gstreamer pipelines (sink & source) */
125 +       GstElement *pipeline[2];
126 +};
127 +
128 +static struct ba_dbus_ctx dbus_ctx;
129 +static GHashTable *workers;
130 +static bool main_loop_on = true;
131 +
132 +static void
133 +main_loop_stop(int sig)
134 +{
135 +       /* Call to this handler restores the default action, so on the
136 +        * second call the program will be forcefully terminated. */
137 +
138 +       struct sigaction sigact = { .sa_handler = SIG_DFL };
139 +       sigaction(sig, &sigact, NULL);
140 +
141 +       main_loop_on = false;
142 +}
143 +
144 +static int
145 +worker_start_pipeline(struct worker *w, int id, int mode, int profile)
146 +{
147 +       GError *gerr = NULL;
148 +       DBusError err = DBUS_ERROR_INIT;
149 +
150 +       if (w->pipeline[id])
151 +               return 0;
152 +
153 +       if (!bluealsa_dbus_pcm_open(&dbus_ctx, w->ba_pcm.pcm_path, mode,
154 +                               &w->ba_pcm_fd, &w->ba_pcm_ctrl_fd, &err)) {
155 +               error("Couldn't open PCM: %s", err.message);
156 +               dbus_error_free(&err);
157 +               goto fail;
158 +       }
159 +
160 +       if (mode == BA_PCM_FLAG_SINK) {
161 +               debug("sink start");
162 +               w->pipeline[id] = gst_parse_launch(
163 +                       /* add a silent live source to ensure a perfect live stream on the
164 +                          output, even when the bt device is not sending or has gaps;
165 +                          this also effectively changes the clock to be the system clock,
166 +                          which is the same clock used by bluez-alsa on the sending side */
167 +                       "audiotestsrc is-live=true wave=silence ! capsfilter name=capsf "
168 +                       "! audiomixer name=m "
169 +                       /* mix the input from bluez-alsa using fdsrc; rawaudioparse
170 +                          is necessary to convert bytes to time and align the buffers */
171 +                       "fdsrc name=fdelem do-timestamp=true ! capsfilter name=capsf2 "
172 +                       "! rawaudioparse use-sink-caps=true ! m. "
173 +                       /* take the mixer output, convert and push to pipewire */
174 +                       "m.src ! capsfilter name=capsf3 ! audioconvert ! audioresample "
175 +                       "! audio/x-raw,format=F32LE,rate=48000 ! pwaudiosink name=pwelem",
176 +                       &gerr);
177 +       } else if (mode == BA_PCM_FLAG_SOURCE && profile == BA_PCM_FLAG_PROFILE_SCO) {
178 +               debug("source start");
179 +               w->pipeline[id] = gst_parse_launch(
180 +                       /* read from pipewire and put the buffers on a leaky queue, which
181 +                          will essentially allow pwaudiosrc to continue working while
182 +                          the fdsink is blocked (when there is no phone call in progress).
183 +                          9600 bytes = 50ms @ F32LE/1ch/48000
184 +                       */
185 +                       "pwaudiosrc name=pwelem ! audio/x-raw,format=F32LE,rate=48000 "
186 +                       "! queue leaky=downstream max-size-time=0 max-size-buffers=0 max-size-bytes=9600 "
187 +                       "! audioconvert ! audioresample ! capsfilter name=capsf "
188 +                       "! fdsink name=fdelem", &gerr);
189 +       }
190 +
191 +       if (gerr) {
192 +               error("Failed to start pipeline: %s", gerr->message);
193 +               g_error_free(gerr);
194 +               goto fail;
195 +       }
196 +
197 +       if (w->pipeline[id]) {
198 +               g_autofree gchar *capsstr = NULL;
199 +               g_autoptr (GstElement) fdelem = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "fdelem");
200 +               g_autoptr (GstElement) pwelem = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "pwelem");
201 +               g_autoptr (GstElement) capsf = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf");
202 +               g_autoptr (GstElement) capsf2 = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf2");
203 +               g_autoptr (GstElement) capsf3 = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf3");
204 +               g_autoptr (GstCaps) caps = gst_caps_new_simple("audio/x-raw",
205 +                               "format", G_TYPE_STRING, "S16LE",
206 +                               "layout", G_TYPE_STRING, "interleaved",
207 +                               "channels", G_TYPE_INT, w->ba_pcm.channels,
208 +                               "rate", G_TYPE_INT, w->ba_pcm.sampling,
209 +                               NULL);
210 +               g_autoptr (GstStructure) stream_props = gst_structure_new("props",
211 +                               "media.role", G_TYPE_STRING, "Communication",
212 +                               "wireplumber.keep-linked", G_TYPE_STRING, "1",
213 +                               NULL);
214 +
215 +               g_object_set(capsf, "caps", caps, NULL);
216 +               if (capsf2)
217 +                       g_object_set(capsf2, "caps", caps, NULL);
218 +               if (capsf3)
219 +                       g_object_set(capsf3, "caps", caps, NULL);
220 +
221 +               capsstr = gst_caps_to_string (caps);
222 +               debug("  caps: %s", capsstr);
223 +
224 +               g_object_set(fdelem, "fd", w->ba_pcm_fd, NULL);
225 +               g_object_set(pwelem, "stream-properties", stream_props, NULL);
226 +
227 +               gst_element_set_state(w->pipeline[id], GST_STATE_PLAYING);
228 +       }
229 +
230 +       return 0;
231 +fail:
232 +       g_clear_object(&w->pipeline[id]);
233 +       return -1;
234 +}
235 +
236 +static int
237 +worker_start(struct worker *w)
238 +{
239 +       int mode = w->ba_pcm.flags & (BA_PCM_FLAG_SOURCE | BA_PCM_FLAG_SINK);
240 +       int profile = w->ba_pcm.flags & (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO);
241 +       /* human-readable BT address */
242 +       char addr[18];
243 +
244 +       g_return_val_if_fail (profile != 0 && profile != (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO), -1);
245 +
246 +       ba2str(&w->ba_pcm.addr, addr);
247 +       debug("%p: worker start addr:%s, mode:0x%x, profile:0x%x", w, addr, mode, profile);
248 +
249 +       if (mode & BA_PCM_FLAG_SINK)
250 +               worker_start_pipeline(w, 0, BA_PCM_FLAG_SINK, profile);
251 +       if (mode & BA_PCM_FLAG_SOURCE)
252 +               worker_start_pipeline(w, 1, BA_PCM_FLAG_SOURCE, profile);
253 +}
254 +
255 +static int
256 +worker_stop(struct worker *w)
257 +{
258 +       debug("stop worker %p", w);
259 +       if (w->pipeline[0]) {
260 +               gst_element_set_state(w->pipeline[0], GST_STATE_NULL);
261 +               g_clear_object(&w->pipeline[0]);
262 +       }
263 +       if (w->pipeline[1]) {
264 +               gst_element_set_state(w->pipeline[1], GST_STATE_NULL);
265 +               g_clear_object(&w->pipeline[1]);
266 +       }
267 +       if (w->ba_pcm_fd != -1) {
268 +               close(w->ba_pcm_fd);
269 +               w->ba_pcm_fd = -1;
270 +       }
271 +       if (w->ba_pcm_ctrl_fd != -1) {
272 +               close(w->ba_pcm_ctrl_fd);
273 +               w->ba_pcm_ctrl_fd = -1;
274 +       }
275 +       return 0;
276 +}
277 +
278 +static int
279 +supervise_pcm_worker(struct worker *worker)
280 +{
281 +       if (worker == NULL)
282 +               return -1;
283 +
284 +       /* no mode? */
285 +       if (worker->ba_pcm.flags & (BA_PCM_FLAG_SOURCE | BA_PCM_FLAG_SINK) == 0)
286 +               goto stop;
287 +
288 +       /* no profile? */
289 +       if (worker->ba_pcm.flags & (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO) == 0)
290 +               goto stop;
291 +
292 +       /* check whether SCO has selected codec */
293 +       if (worker->ba_pcm.flags & BA_PCM_FLAG_PROFILE_SCO &&
294 +                       worker->ba_pcm.codec == 0) {
295 +               debug("Skipping SCO with codec not selected");
296 +               goto stop;
297 +       }
298 +
299 +start:
300 +       return worker_start(worker);
301 +stop:
302 +       return worker_stop(worker);
303 +}
304 +
305 +static void
306 +worker_new(struct ba_pcm *pcm)
307 +{
308 +       struct worker *w = g_slice_new0 (struct worker);
309 +       memcpy(&w->ba_pcm, pcm, sizeof(struct ba_pcm));
310 +       w->ba_pcm_fd = -1;
311 +       w->ba_pcm_ctrl_fd = -1;
312 +       g_hash_table_insert(workers, w->ba_pcm.pcm_path, w);
313 +       supervise_pcm_worker(w);
314 +}
315 +
316 +static DBusHandlerResult
317 +dbus_signal_handler(DBusConnection *conn, DBusMessage *message, void *data)
318 +{
319 +       (void)conn;
320 +       (void)data;
321 +
322 +       const char *path = dbus_message_get_path(message);
323 +       const char *interface = dbus_message_get_interface(message);
324 +       const char *signal = dbus_message_get_member(message);
325 +
326 +       DBusMessageIter iter;
327 +       struct worker *worker;
328 +
329 +       if (strcmp(interface, BLUEALSA_INTERFACE_MANAGER) == 0) {
330 +
331 +               if (strcmp(signal, "PCMAdded") == 0) {
332 +                       struct ba_pcm pcm;
333 +                       if (!dbus_message_iter_init(message, &iter) ||
334 +                                       !bluealsa_dbus_message_iter_get_pcm(&iter, NULL, &pcm)) {
335 +                               error("Couldn't add new PCM: %s", "Invalid signal signature");
336 +                               goto fail;
337 +                       }
338 +                       worker_new(&pcm);
339 +                       return DBUS_HANDLER_RESULT_HANDLED;
340 +               }
341 +
342 +               if (strcmp(signal, "PCMRemoved") == 0) {
343 +                       if (!dbus_message_iter_init(message, &iter) ||
344 +                                       dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) {
345 +                               error("Couldn't remove PCM: %s", "Invalid signal signature");
346 +                               goto fail;
347 +                       }
348 +                       dbus_message_iter_get_basic(&iter, &path);
349 +                       g_hash_table_remove(workers, path);
350 +                       return DBUS_HANDLER_RESULT_HANDLED;
351 +               }
352 +
353 +       }
354 +
355 +       if (strcmp(interface, DBUS_INTERFACE_PROPERTIES) == 0) {
356 +               worker = g_hash_table_lookup(workers, path);
357 +               if (!worker)
358 +                       goto fail;
359 +               if (!dbus_message_iter_init(message, &iter) ||
360 +                               dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) {
361 +                       error("Couldn't update PCM: %s", "Invalid signal signature");
362 +                       goto fail;
363 +               }
364 +               dbus_message_iter_get_basic(&iter, &interface);
365 +               dbus_message_iter_next(&iter);
366 +               if (!bluealsa_dbus_message_iter_get_pcm_props(&iter, NULL, &worker->ba_pcm))
367 +                       goto fail;
368 +               supervise_pcm_worker(worker);
369 +               return DBUS_HANDLER_RESULT_HANDLED;
370 +       }
371 +
372 +fail:
373 +       return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
374 +}
375 +
376 +static void
377 +destroy_worker(void *worker)
378 +{
379 +       struct worker *w = worker;
380 +       worker_stop(w);
381 +       g_slice_free(struct worker, w);
382 +}
383 +
384 +int
385 +main(int argc, char *argv[])
386 +{
387 +       int ret = EXIT_SUCCESS;
388 +
389 +       log_open(argv[0], false, false);
390 +       gst_init(&argc, &argv);
391 +       dbus_threads_init_default();
392 +
393 +       DBusError err = DBUS_ERROR_INIT;
394 +       if (!bluealsa_dbus_connection_ctx_init(&dbus_ctx, BLUEALSA_SERVICE, &err)) {
395 +               error("Couldn't initialize D-Bus context: %s", err.message);
396 +               return EXIT_FAILURE;
397 +       }
398 +
399 +       bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
400 +                       BLUEALSA_SERVICE, NULL, BLUEALSA_INTERFACE_MANAGER, "PCMAdded", NULL);
401 +       bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
402 +                       BLUEALSA_SERVICE, NULL, BLUEALSA_INTERFACE_MANAGER, "PCMRemoved", NULL);
403 +       bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
404 +                       BLUEALSA_SERVICE, NULL, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged",
405 +                       "arg0='"BLUEALSA_INTERFACE_PCM"'");
406 +
407 +       if (!dbus_connection_add_filter(dbus_ctx.conn, dbus_signal_handler, NULL, NULL)) {
408 +               error("Couldn't add D-Bus filter: %s", err.message);
409 +               return EXIT_FAILURE;
410 +       }
411 +
412 +       workers = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, destroy_worker);
413 +
414 +       {
415 +               struct ba_pcm *pcms = NULL;
416 +               size_t pcms_count = 0, i;
417 +
418 +               if (!bluealsa_dbus_get_pcms(&dbus_ctx, &pcms, &pcms_count, &err))
419 +                       warn("Couldn't get BlueALSA PCM list: %s", err.message);
420 +
421 +               for (i = 0; i < pcms_count; i++) {
422 +                       worker_new(&pcms[i]);
423 +               }
424 +
425 +               free(pcms);
426 +       }
427 +
428 +       struct sigaction sigact = { .sa_handler = main_loop_stop };
429 +       sigaction(SIGTERM, &sigact, NULL);
430 +       sigaction(SIGINT, &sigact, NULL);
431 +
432 +       /* Ignore SIGPIPE, which may be received when writing to the bluealsa
433 +          socket when it is closed on the remote end */
434 +       signal(SIGPIPE, SIG_IGN);
435 +
436 +       debug("Starting main loop");
437 +       while (main_loop_on) {
438 +
439 +               struct pollfd pfds[10];
440 +               nfds_t pfds_len = ARRAYSIZE(pfds);
441 +
442 +               if (!bluealsa_dbus_connection_poll_fds(&dbus_ctx, pfds, &pfds_len)) {
443 +                       error("Couldn't get D-Bus connection file descriptors");
444 +                       ret = EXIT_FAILURE;
445 +                       goto out;
446 +               }
447 +
448 +               if (poll(pfds, pfds_len, -1) == -1 &&
449 +                               errno == EINTR)
450 +                       continue;
451 +
452 +               if (bluealsa_dbus_connection_poll_dispatch(&dbus_ctx, pfds, pfds_len))
453 +                       while (dbus_connection_dispatch(dbus_ctx.conn) == DBUS_DISPATCH_DATA_REMAINS)
454 +                               continue;
455 +
456 +       }
457 +
458 +out:
459 +       g_hash_table_unref(workers);
460 +       return ret;
461 +}
462 -- 
463 2.23.0
464