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
7 Unfortunately, the bluez-alsa PCM plugin does not work correctly
8 when it is used through pipewire (or gstreamer, or anywhere really...).
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.
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.
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.
23 Upstream-Status: Inappropriate
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
31 diff --git a/configure.ac b/configure.ac
32 index 4825afa..9125871 100644
35 @@ -141,6 +141,13 @@ AM_COND_IF([ENABLE_HCITOP], [
36 PKG_CHECK_MODULES([NCURSES], [ncurses])
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])
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 = \
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 \
65 +bluealsa_gst_helper_CFLAGS = \
66 + -I$(top_srcdir)/src \
71 +bluealsa_gst_helper_LDADD = \
77 diff --git a/utils/gst-helper.c b/utils/gst-helper.c
79 index 0000000..1b021ee
81 +++ b/utils/gst-helper.c
83 +/* Bluez-Alsa PipeWire integration GStreamer helper
85 + * Copyright © 2016-2019 Arkadiusz Bokowy
86 + * Copyright © 2019 Collabora Ltd.
87 + * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
89 + * SPDX-License-Identifier: MIT
101 +#include <stdbool.h>
108 +#include <bluetooth/bluetooth.h>
109 +#include <dbus/dbus.h>
110 +#include <gst/gst.h>
112 +#include "shared/dbus-client.h"
113 +#include "shared/defs.h"
114 +#include "shared/ffb.h"
115 +#include "shared/log.h"
118 + /* used BlueALSA PCM device */
119 + struct ba_pcm ba_pcm;
120 + /* file descriptor of PCM FIFO */
122 + /* file descriptor of PCM control */
123 + int ba_pcm_ctrl_fd;
124 + /* the gstreamer pipelines (sink & source) */
125 + GstElement *pipeline[2];
128 +static struct ba_dbus_ctx dbus_ctx;
129 +static GHashTable *workers;
130 +static bool main_loop_on = true;
133 +main_loop_stop(int sig)
135 + /* Call to this handler restores the default action, so on the
136 + * second call the program will be forcefully terminated. */
138 + struct sigaction sigact = { .sa_handler = SIG_DFL };
139 + sigaction(sig, &sigact, NULL);
141 + main_loop_on = false;
145 +worker_start_pipeline(struct worker *w, int id, int mode, int profile)
147 + GError *gerr = NULL;
148 + DBusError err = DBUS_ERROR_INIT;
150 + if (w->pipeline[id])
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);
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",
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
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);
192 + error("Failed to start pipeline: %s", gerr->message);
193 + g_error_free(gerr);
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,
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",
215 + g_object_set(capsf, "caps", caps, NULL);
217 + g_object_set(capsf2, "caps", caps, NULL);
219 + g_object_set(capsf3, "caps", caps, NULL);
221 + capsstr = gst_caps_to_string (caps);
222 + debug(" caps: %s", capsstr);
224 + g_object_set(fdelem, "fd", w->ba_pcm_fd, NULL);
225 + g_object_set(pwelem, "stream-properties", stream_props, NULL);
227 + gst_element_set_state(w->pipeline[id], GST_STATE_PLAYING);
232 + g_clear_object(&w->pipeline[id]);
237 +worker_start(struct worker *w)
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 */
244 + g_return_val_if_fail (profile != 0 && profile != (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO), -1);
246 + ba2str(&w->ba_pcm.addr, addr);
247 + debug("%p: worker start addr:%s, mode:0x%x, profile:0x%x", w, addr, mode, profile);
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);
256 +worker_stop(struct worker *w)
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]);
263 + if (w->pipeline[1]) {
264 + gst_element_set_state(w->pipeline[1], GST_STATE_NULL);
265 + g_clear_object(&w->pipeline[1]);
267 + if (w->ba_pcm_fd != -1) {
268 + close(w->ba_pcm_fd);
271 + if (w->ba_pcm_ctrl_fd != -1) {
272 + close(w->ba_pcm_ctrl_fd);
273 + w->ba_pcm_ctrl_fd = -1;
279 +supervise_pcm_worker(struct worker *worker)
281 + if (worker == NULL)
285 + if (worker->ba_pcm.flags & (BA_PCM_FLAG_SOURCE | BA_PCM_FLAG_SINK) == 0)
289 + if (worker->ba_pcm.flags & (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO) == 0)
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");
300 + return worker_start(worker);
302 + return worker_stop(worker);
306 +worker_new(struct ba_pcm *pcm)
308 + struct worker *w = g_slice_new0 (struct worker);
309 + memcpy(&w->ba_pcm, pcm, sizeof(struct ba_pcm));
311 + w->ba_pcm_ctrl_fd = -1;
312 + g_hash_table_insert(workers, w->ba_pcm.pcm_path, w);
313 + supervise_pcm_worker(w);
316 +static DBusHandlerResult
317 +dbus_signal_handler(DBusConnection *conn, DBusMessage *message, void *data)
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);
326 + DBusMessageIter iter;
327 + struct worker *worker;
329 + if (strcmp(interface, BLUEALSA_INTERFACE_MANAGER) == 0) {
331 + if (strcmp(signal, "PCMAdded") == 0) {
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");
339 + return DBUS_HANDLER_RESULT_HANDLED;
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");
348 + dbus_message_iter_get_basic(&iter, &path);
349 + g_hash_table_remove(workers, path);
350 + return DBUS_HANDLER_RESULT_HANDLED;
355 + if (strcmp(interface, DBUS_INTERFACE_PROPERTIES) == 0) {
356 + worker = g_hash_table_lookup(workers, path);
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");
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))
368 + supervise_pcm_worker(worker);
369 + return DBUS_HANDLER_RESULT_HANDLED;
373 + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
377 +destroy_worker(void *worker)
379 + struct worker *w = worker;
381 + g_slice_free(struct worker, w);
385 +main(int argc, char *argv[])
387 + int ret = EXIT_SUCCESS;
389 + log_open(argv[0], false, false);
390 + gst_init(&argc, &argv);
391 + dbus_threads_init_default();
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;
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"'");
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;
412 + workers = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, destroy_worker);
415 + struct ba_pcm *pcms = NULL;
416 + size_t pcms_count = 0, i;
418 + if (!bluealsa_dbus_get_pcms(&dbus_ctx, &pcms, &pcms_count, &err))
419 + warn("Couldn't get BlueALSA PCM list: %s", err.message);
421 + for (i = 0; i < pcms_count; i++) {
422 + worker_new(&pcms[i]);
428 + struct sigaction sigact = { .sa_handler = main_loop_stop };
429 + sigaction(SIGTERM, &sigact, NULL);
430 + sigaction(SIGINT, &sigact, NULL);
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);
436 + debug("Starting main loop");
437 + while (main_loop_on) {
439 + struct pollfd pfds[10];
440 + nfds_t pfds_len = ARRAYSIZE(pfds);
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;
448 + if (poll(pfds, pfds_len, -1) == -1 &&
452 + if (bluealsa_dbus_connection_poll_dispatch(&dbus_ctx, pfds, pfds_len))
453 + while (dbus_connection_dispatch(dbus_ctx.conn) == DBUS_DISPATCH_DATA_REMAINS)
459 + g_hash_table_unref(workers);