summaryrefslogtreecommitdiffstats
path: root/meta-pipewire/recipes-connectivity/bluez-alsa
diff options
context:
space:
mode:
authorGeorge Kiagiadakis <george.kiagiadakis@collabora.com>2019-10-04 23:07:46 +0300
committerGeorge Kiagiadakis <george.kiagiadakis@collabora.com>2019-10-05 09:44:58 +0300
commit48e10ccbf966b091de28ad60f3a5983f30a8a956 (patch)
treedb729936e866c3d96054688b4afd0f23fd27b58e /meta-pipewire/recipes-connectivity/bluez-alsa
parent2a069d5124121336a77573e95dde9c8c34b872ec (diff)
meta-pipewire: add recipe to build the bluez-alsa-pipewire gstreamer helperhalibut_8.0.2halibut/8.0.28.0.2
Unfortunately, the bluez-alsa PCM plugin does not work correctly when it is used through pipewire (or gstreamer, or anywhere really...). For this reason I have built a helper client that uses GStreamer to glue together the bluez-alsa sockets with pipewire. This helper is implemented as a patch to bluez-alsa so that it can use its internal private API. In the future this needs some re-thinking The helper is meant to run in the background as a service and it will create the appropriate streams in pipewire when it detects a new device on the bluealsa d-bus interface. Currently it only supports a2dp-sink and hfp modes (i.e. media player from a phone + calls). Bluetooth speakers need further policy work in wireplumber that is too complex to support on the current halibut version of pipewire/wireplumber. Bug-AGL: SPEC-2792 Change-Id: Ia0a0c4741dc6f28958e911436edde17ebde1a434 Signed-off-by: George Kiagiadakis <george.kiagiadakis@collabora.com>
Diffstat (limited to 'meta-pipewire/recipes-connectivity/bluez-alsa')
-rw-r--r--meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/0001-utils-add-a-gstreamer-helper-application-for-interco.patch464
-rw-r--r--meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/bluealsa-gst-helper@.service18
-rw-r--r--meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa_git.bbappend35
3 files changed, 517 insertions, 0 deletions
diff --git a/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/0001-utils-add-a-gstreamer-helper-application-for-interco.patch b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/0001-utils-add-a-gstreamer-helper-application-for-interco.patch
new file mode 100644
index 00000000..37c03218
--- /dev/null
+++ b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/0001-utils-add-a-gstreamer-helper-application-for-interco.patch
@@ -0,0 +1,464 @@
+From 33555a493af67f3acc2129764a1b093aec6254d8 Mon Sep 17 00:00:00 2001
+From: George Kiagiadakis <george.kiagiadakis@collabora.com>
+Date: Fri, 4 Oct 2019 20:51:24 +0300
+Subject: [PATCH] utils: add a gstreamer helper application for interconnection
+ with pipewire
+
+Unfortunately, the bluez-alsa PCM plugin does not work correctly
+when it is used through pipewire (or gstreamer, or anywhere really...).
+
+Thanfully, the bluez-alsa PCM plugin is only a simple client that
+reads/writes on a file descriptor that was opened by bluealsa.
+This allows us to use bluealsa without the PCM plugin, just like it
+is done in the aplay.c util.
+
+This one uses GStreamer to implement the plumbing between pipewire
+and the file descriptor. On the reading side we are also doing some
+tricks to ensure a smooth stream, which is not the case for the
+stream that is coming out of bluealsa.
+
+This helper is implemented as a patch to bluez-alsa so that it can
+use its internal private API. In the future this needs some re-thinking.
+
+Upstream-Status: Inappropriate
+---
+ configure.ac | 7 +
+ utils/Makefile.am | 20 +++
+ utils/gst-helper.c | 379 +++++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 406 insertions(+)
+ create mode 100644 utils/gst-helper.c
+
+diff --git a/configure.ac b/configure.ac
+index 4825afa..9125871 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -141,6 +141,13 @@ AM_COND_IF([ENABLE_HCITOP], [
+ PKG_CHECK_MODULES([NCURSES], [ncurses])
+ ])
+
++AC_ARG_ENABLE([gsthelper],
++ [AS_HELP_STRING([--enable-gsthelper], [enable building of gsthelper tool])])
++AM_CONDITIONAL([ENABLE_GSTHELPER], [test "x$enable_gsthelper" = "xyes"])
++AM_COND_IF([ENABLE_GSTHELPER], [
++ PKG_CHECK_MODULES([GST], [gstreamer-1.0 glib-2.0])
++])
++
+ AC_ARG_ENABLE([test],
+ [AS_HELP_STRING([--enable-test], [enable unit test])])
+ AM_CONDITIONAL([ENABLE_TEST], [test "x$enable_test" = "xyes"])
+diff --git a/utils/Makefile.am b/utils/Makefile.am
+index 9057f2c..9790474 100644
+--- a/utils/Makefile.am
++++ b/utils/Makefile.am
+@@ -47,3 +47,23 @@ hcitop_LDADD = \
+ @LIBBSD_LIBS@ \
+ @NCURSES_LIBS@
+ endif
++
++if ENABLE_GSTHELPER
++bin_PROGRAMS += bluealsa-gst-helper
++bluealsa_gst_helper_SOURCES = \
++ ../src/shared/dbus-client.c \
++ ../src/shared/ffb.c \
++ ../src/shared/log.c \
++ gst-helper.c
++bluealsa_gst_helper_CFLAGS = \
++ -I$(top_srcdir)/src \
++ @ALSA_CFLAGS@ \
++ @BLUEZ_CFLAGS@ \
++ @DBUS1_CFLAGS@ \
++ @GST_CFLAGS@
++bluealsa_gst_helper_LDADD = \
++ @ALSA_LIBS@ \
++ @BLUEZ_LIBS@ \
++ @DBUS1_LIBS@ \
++ @GST_LIBS@
++endif
+diff --git a/utils/gst-helper.c b/utils/gst-helper.c
+new file mode 100644
+index 0000000..1b021ee
+--- /dev/null
++++ b/utils/gst-helper.c
+@@ -0,0 +1,379 @@
++/* Bluez-Alsa PipeWire integration GStreamer helper
++ *
++ * Copyright © 2016-2019 Arkadiusz Bokowy
++ * Copyright © 2019 Collabora Ltd.
++ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
++ *
++ * SPDX-License-Identifier: MIT
++ */
++
++#if HAVE_CONFIG_H
++# include <config.h>
++#endif
++
++#include <errno.h>
++#include <getopt.h>
++#include <poll.h>
++#include <pthread.h>
++#include <signal.h>
++#include <stdbool.h>
++#include <stdint.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <string.h>
++#include <unistd.h>
++
++#include <bluetooth/bluetooth.h>
++#include <dbus/dbus.h>
++#include <gst/gst.h>
++
++#include "shared/dbus-client.h"
++#include "shared/defs.h"
++#include "shared/ffb.h"
++#include "shared/log.h"
++
++struct worker {
++ /* used BlueALSA PCM device */
++ struct ba_pcm ba_pcm;
++ /* file descriptor of PCM FIFO */
++ int ba_pcm_fd;
++ /* file descriptor of PCM control */
++ int ba_pcm_ctrl_fd;
++ /* the gstreamer pipelines (sink & source) */
++ GstElement *pipeline[2];
++};
++
++static struct ba_dbus_ctx dbus_ctx;
++static GHashTable *workers;
++static bool main_loop_on = true;
++
++static void
++main_loop_stop(int sig)
++{
++ /* Call to this handler restores the default action, so on the
++ * second call the program will be forcefully terminated. */
++
++ struct sigaction sigact = { .sa_handler = SIG_DFL };
++ sigaction(sig, &sigact, NULL);
++
++ main_loop_on = false;
++}
++
++static int
++worker_start_pipeline(struct worker *w, int id, int mode, int profile)
++{
++ GError *gerr = NULL;
++ DBusError err = DBUS_ERROR_INIT;
++
++ if (w->pipeline[id])
++ return 0;
++
++ if (!bluealsa_dbus_pcm_open(&dbus_ctx, w->ba_pcm.pcm_path, mode,
++ &w->ba_pcm_fd, &w->ba_pcm_ctrl_fd, &err)) {
++ error("Couldn't open PCM: %s", err.message);
++ dbus_error_free(&err);
++ goto fail;
++ }
++
++ if (mode == BA_PCM_FLAG_SINK) {
++ debug("sink start");
++ w->pipeline[id] = gst_parse_launch(
++ /* add a silent live source to ensure a perfect live stream on the
++ output, even when the bt device is not sending or has gaps;
++ this also effectively changes the clock to be the system clock,
++ which is the same clock used by bluez-alsa on the sending side */
++ "audiotestsrc is-live=true wave=silence ! capsfilter name=capsf "
++ "! audiomixer name=m "
++ /* mix the input from bluez-alsa using fdsrc; rawaudioparse
++ is necessary to convert bytes to time and align the buffers */
++ "fdsrc name=fdelem do-timestamp=true ! capsfilter name=capsf2 "
++ "! rawaudioparse use-sink-caps=true ! m. "
++ /* take the mixer output, convert and push to pipewire */
++ "m.src ! capsfilter name=capsf3 ! audioconvert ! audioresample "
++ "! audio/x-raw,format=F32LE,rate=48000 ! pwaudiosink name=pwelem",
++ &gerr);
++ } else if (mode == BA_PCM_FLAG_SOURCE && profile == BA_PCM_FLAG_PROFILE_SCO) {
++ debug("source start");
++ w->pipeline[id] = gst_parse_launch(
++ /* read from pipewire and put the buffers on a leaky queue, which
++ will essentially allow pwaudiosrc to continue working while
++ the fdsink is blocked (when there is no phone call in progress).
++ 9600 bytes = 50ms @ F32LE/1ch/48000
++ */
++ "pwaudiosrc name=pwelem ! audio/x-raw,format=F32LE,rate=48000 "
++ "! queue leaky=downstream max-size-time=0 max-size-buffers=0 max-size-bytes=9600 "
++ "! audioconvert ! audioresample ! capsfilter name=capsf "
++ "! fdsink name=fdelem", &gerr);
++ }
++
++ if (gerr) {
++ error("Failed to start pipeline: %s", gerr->message);
++ g_error_free(gerr);
++ goto fail;
++ }
++
++ if (w->pipeline[id]) {
++ g_autofree gchar *capsstr = NULL;
++ g_autoptr (GstElement) fdelem = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "fdelem");
++ g_autoptr (GstElement) pwelem = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "pwelem");
++ g_autoptr (GstElement) capsf = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf");
++ g_autoptr (GstElement) capsf2 = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf2");
++ g_autoptr (GstElement) capsf3 = gst_bin_get_by_name(GST_BIN(w->pipeline[id]), "capsf3");
++ g_autoptr (GstCaps) caps = gst_caps_new_simple("audio/x-raw",
++ "format", G_TYPE_STRING, "S16LE",
++ "layout", G_TYPE_STRING, "interleaved",
++ "channels", G_TYPE_INT, w->ba_pcm.channels,
++ "rate", G_TYPE_INT, w->ba_pcm.sampling,
++ NULL);
++ g_autoptr (GstStructure) stream_props = gst_structure_new("props",
++ "media.role", G_TYPE_STRING, "Communication",
++ "wireplumber.keep-linked", G_TYPE_STRING, "1",
++ NULL);
++
++ g_object_set(capsf, "caps", caps, NULL);
++ if (capsf2)
++ g_object_set(capsf2, "caps", caps, NULL);
++ if (capsf3)
++ g_object_set(capsf3, "caps", caps, NULL);
++
++ capsstr = gst_caps_to_string (caps);
++ debug(" caps: %s", capsstr);
++
++ g_object_set(fdelem, "fd", w->ba_pcm_fd, NULL);
++ g_object_set(pwelem, "stream-properties", stream_props, NULL);
++
++ gst_element_set_state(w->pipeline[id], GST_STATE_PLAYING);
++ }
++
++ return 0;
++fail:
++ g_clear_object(&w->pipeline[id]);
++ return -1;
++}
++
++static int
++worker_start(struct worker *w)
++{
++ int mode = w->ba_pcm.flags & (BA_PCM_FLAG_SOURCE | BA_PCM_FLAG_SINK);
++ int profile = w->ba_pcm.flags & (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO);
++ /* human-readable BT address */
++ char addr[18];
++
++ g_return_val_if_fail (profile != 0 && profile != (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO), -1);
++
++ ba2str(&w->ba_pcm.addr, addr);
++ debug("%p: worker start addr:%s, mode:0x%x, profile:0x%x", w, addr, mode, profile);
++
++ if (mode & BA_PCM_FLAG_SINK)
++ worker_start_pipeline(w, 0, BA_PCM_FLAG_SINK, profile);
++ if (mode & BA_PCM_FLAG_SOURCE)
++ worker_start_pipeline(w, 1, BA_PCM_FLAG_SOURCE, profile);
++}
++
++static int
++worker_stop(struct worker *w)
++{
++ debug("stop worker %p", w);
++ if (w->pipeline[0]) {
++ gst_element_set_state(w->pipeline[0], GST_STATE_NULL);
++ g_clear_object(&w->pipeline[0]);
++ }
++ if (w->pipeline[1]) {
++ gst_element_set_state(w->pipeline[1], GST_STATE_NULL);
++ g_clear_object(&w->pipeline[1]);
++ }
++ if (w->ba_pcm_fd != -1) {
++ close(w->ba_pcm_fd);
++ w->ba_pcm_fd = -1;
++ }
++ if (w->ba_pcm_ctrl_fd != -1) {
++ close(w->ba_pcm_ctrl_fd);
++ w->ba_pcm_ctrl_fd = -1;
++ }
++ return 0;
++}
++
++static int
++supervise_pcm_worker(struct worker *worker)
++{
++ if (worker == NULL)
++ return -1;
++
++ /* no mode? */
++ if (worker->ba_pcm.flags & (BA_PCM_FLAG_SOURCE | BA_PCM_FLAG_SINK) == 0)
++ goto stop;
++
++ /* no profile? */
++ if (worker->ba_pcm.flags & (BA_PCM_FLAG_PROFILE_A2DP | BA_PCM_FLAG_PROFILE_SCO) == 0)
++ goto stop;
++
++ /* check whether SCO has selected codec */
++ if (worker->ba_pcm.flags & BA_PCM_FLAG_PROFILE_SCO &&
++ worker->ba_pcm.codec == 0) {
++ debug("Skipping SCO with codec not selected");
++ goto stop;
++ }
++
++start:
++ return worker_start(worker);
++stop:
++ return worker_stop(worker);
++}
++
++static void
++worker_new(struct ba_pcm *pcm)
++{
++ struct worker *w = g_slice_new0 (struct worker);
++ memcpy(&w->ba_pcm, pcm, sizeof(struct ba_pcm));
++ w->ba_pcm_fd = -1;
++ w->ba_pcm_ctrl_fd = -1;
++ g_hash_table_insert(workers, w->ba_pcm.pcm_path, w);
++ supervise_pcm_worker(w);
++}
++
++static DBusHandlerResult
++dbus_signal_handler(DBusConnection *conn, DBusMessage *message, void *data)
++{
++ (void)conn;
++ (void)data;
++
++ const char *path = dbus_message_get_path(message);
++ const char *interface = dbus_message_get_interface(message);
++ const char *signal = dbus_message_get_member(message);
++
++ DBusMessageIter iter;
++ struct worker *worker;
++
++ if (strcmp(interface, BLUEALSA_INTERFACE_MANAGER) == 0) {
++
++ if (strcmp(signal, "PCMAdded") == 0) {
++ struct ba_pcm pcm;
++ if (!dbus_message_iter_init(message, &iter) ||
++ !bluealsa_dbus_message_iter_get_pcm(&iter, NULL, &pcm)) {
++ error("Couldn't add new PCM: %s", "Invalid signal signature");
++ goto fail;
++ }
++ worker_new(&pcm);
++ return DBUS_HANDLER_RESULT_HANDLED;
++ }
++
++ if (strcmp(signal, "PCMRemoved") == 0) {
++ if (!dbus_message_iter_init(message, &iter) ||
++ dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) {
++ error("Couldn't remove PCM: %s", "Invalid signal signature");
++ goto fail;
++ }
++ dbus_message_iter_get_basic(&iter, &path);
++ g_hash_table_remove(workers, path);
++ return DBUS_HANDLER_RESULT_HANDLED;
++ }
++
++ }
++
++ if (strcmp(interface, DBUS_INTERFACE_PROPERTIES) == 0) {
++ worker = g_hash_table_lookup(workers, path);
++ if (!worker)
++ goto fail;
++ if (!dbus_message_iter_init(message, &iter) ||
++ dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) {
++ error("Couldn't update PCM: %s", "Invalid signal signature");
++ goto fail;
++ }
++ dbus_message_iter_get_basic(&iter, &interface);
++ dbus_message_iter_next(&iter);
++ if (!bluealsa_dbus_message_iter_get_pcm_props(&iter, NULL, &worker->ba_pcm))
++ goto fail;
++ supervise_pcm_worker(worker);
++ return DBUS_HANDLER_RESULT_HANDLED;
++ }
++
++fail:
++ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
++}
++
++static void
++destroy_worker(void *worker)
++{
++ struct worker *w = worker;
++ worker_stop(w);
++ g_slice_free(struct worker, w);
++}
++
++int
++main(int argc, char *argv[])
++{
++ int ret = EXIT_SUCCESS;
++
++ log_open(argv[0], false, false);
++ gst_init(&argc, &argv);
++ dbus_threads_init_default();
++
++ DBusError err = DBUS_ERROR_INIT;
++ if (!bluealsa_dbus_connection_ctx_init(&dbus_ctx, BLUEALSA_SERVICE, &err)) {
++ error("Couldn't initialize D-Bus context: %s", err.message);
++ return EXIT_FAILURE;
++ }
++
++ bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
++ BLUEALSA_SERVICE, NULL, BLUEALSA_INTERFACE_MANAGER, "PCMAdded", NULL);
++ bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
++ BLUEALSA_SERVICE, NULL, BLUEALSA_INTERFACE_MANAGER, "PCMRemoved", NULL);
++ bluealsa_dbus_connection_signal_match_add(&dbus_ctx,
++ BLUEALSA_SERVICE, NULL, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged",
++ "arg0='"BLUEALSA_INTERFACE_PCM"'");
++
++ if (!dbus_connection_add_filter(dbus_ctx.conn, dbus_signal_handler, NULL, NULL)) {
++ error("Couldn't add D-Bus filter: %s", err.message);
++ return EXIT_FAILURE;
++ }
++
++ workers = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, destroy_worker);
++
++ {
++ struct ba_pcm *pcms = NULL;
++ size_t pcms_count = 0, i;
++
++ if (!bluealsa_dbus_get_pcms(&dbus_ctx, &pcms, &pcms_count, &err))
++ warn("Couldn't get BlueALSA PCM list: %s", err.message);
++
++ for (i = 0; i < pcms_count; i++) {
++ worker_new(&pcms[i]);
++ }
++
++ free(pcms);
++ }
++
++ struct sigaction sigact = { .sa_handler = main_loop_stop };
++ sigaction(SIGTERM, &sigact, NULL);
++ sigaction(SIGINT, &sigact, NULL);
++
++ /* Ignore SIGPIPE, which may be received when writing to the bluealsa
++ socket when it is closed on the remote end */
++ signal(SIGPIPE, SIG_IGN);
++
++ debug("Starting main loop");
++ while (main_loop_on) {
++
++ struct pollfd pfds[10];
++ nfds_t pfds_len = ARRAYSIZE(pfds);
++
++ if (!bluealsa_dbus_connection_poll_fds(&dbus_ctx, pfds, &pfds_len)) {
++ error("Couldn't get D-Bus connection file descriptors");
++ ret = EXIT_FAILURE;
++ goto out;
++ }
++
++ if (poll(pfds, pfds_len, -1) == -1 &&
++ errno == EINTR)
++ continue;
++
++ if (bluealsa_dbus_connection_poll_dispatch(&dbus_ctx, pfds, pfds_len))
++ while (dbus_connection_dispatch(dbus_ctx.conn) == DBUS_DISPATCH_DATA_REMAINS)
++ continue;
++
++ }
++
++out:
++ g_hash_table_unref(workers);
++ return ret;
++}
+--
+2.23.0
+
diff --git a/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/bluealsa-gst-helper@.service b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/bluealsa-gst-helper@.service
new file mode 100644
index 00000000..495ab622
--- /dev/null
+++ b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa/bluealsa-gst-helper@.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=Bluetooth audio helper for user %i
+Requires=pipewire@%i.socket bluez-alsa.service
+After=pipewire@%i.socket bluez-alsa.service
+
+[Service]
+Type=simple
+Restart=on-failure
+ExecStart=/usr/bin/bluealsa-gst-helper
+
+Environment=XDG_RUNTIME_DIR=/run/user/%i
+Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%i/bus
+
+User=%i
+Slice=user-%i.slice
+SupplementaryGroups=audio
+UMask=0077
+CapabilityBoundingSet=
diff --git a/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa_git.bbappend b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa_git.bbappend
new file mode 100644
index 00000000..2f9699a8
--- /dev/null
+++ b/meta-pipewire/recipes-connectivity/bluez-alsa/bluez-alsa_git.bbappend
@@ -0,0 +1,35 @@
+FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"
+
+SRC_URI += "\
+ file://0001-utils-add-a-gstreamer-helper-application-for-interco.patch \
+ file://bluealsa-gst-helper@.service \
+ "
+
+PACKAGECONFIG += "gsthelper"
+PACKAGECONFIG[gsthelper] = "--enable-gsthelper, --disable-gsthelper, gstreamer1.0"
+
+do_install_append() {
+ if ${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'true', 'false', d)}; then
+ # install the service file
+ mkdir -p ${D}${systemd_system_unitdir}/
+ install -m 0644 ${WORKDIR}/bluealsa-gst-helper@.service ${D}${systemd_system_unitdir}/bluealsa-gst-helper@.service
+
+ # enable the helper to start together with afm-user-session
+ mkdir -p ${D}${systemd_system_unitdir}/afm-user-session@.target.wants
+ ln -sf ../bluealsa-gst-helper@.service ${D}${systemd_system_unitdir}/afm-user-session@.target.wants/bluealsa-gst-helper@.service
+ fi
+}
+
+PACKAGES =+ "${PN}-pipewire"
+
+FILES_${PN}-pipewire = "\
+ ${bindir}/bluealsa-gst-helper \
+ ${systemd_system_unitdir}/bluealsa-gst-helper@.service \
+ ${systemd_system_unitdir}/afm-user-session@.target.wants/bluealsa-gst-helper@.service \
+ "
+RDEPENDS_${PN}-pipewire += "\
+ bluez-alsa \
+ pipewire \
+ gstreamer1.0-plugins-base \
+ gstreamer1.0-pipewire \
+ "