mirror of
https://codeberg.org/dnkl/yambar.git
synced 2025-04-27 12:25:39 +02:00
Add initial pulseaudio module support
This seems to work quite well including hotplugging of devices, changes to default sinks/sources etc. So far I only provided percentage values for sink/source since they seem most useful. Defaults are set to DEFAULT_SINK/DEFAULT_SOURCE which means it will follow whatever defaults are at the moment (instead of being fixed to a given device). One thing currently left unhandled is when pulseaudio/pipewire gets disconnected/restarted. In such a case we mark the sink/source as offline but there is no reconnect attempted later. Added man page + more complex example which includes mixed usage of both input and output.
This commit is contained in:
parent
6250360c58
commit
04d47cba3c
8 changed files with 566 additions and 2 deletions
|
@ -9,6 +9,7 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd',
|
||||||
'yambar-modules-foreign-toplevel.5.scd',
|
'yambar-modules-foreign-toplevel.5.scd',
|
||||||
'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd',
|
'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd',
|
||||||
'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd',
|
'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd',
|
||||||
|
'yambar-modules-pulse.5.scd',
|
||||||
'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd',
|
'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd',
|
||||||
'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd',
|
'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd',
|
||||||
'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd',
|
'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd',
|
||||||
|
|
62
doc/yambar-modules-pulse.5.scd
Normal file
62
doc/yambar-modules-pulse.5.scd
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
yambar-modules-pulse(5)
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
pulse - Monitors one pulseaudio source and sink for volume and mute/unmute changes
|
||||||
|
|
||||||
|
# TAGS
|
||||||
|
|
||||||
|
[[ *Name*
|
||||||
|
:[ *Type*
|
||||||
|
:[ *Description*
|
||||||
|
| online
|
||||||
|
: bool
|
||||||
|
: True when the Pulseaudio connection has successfully been opened
|
||||||
|
| sink_online
|
||||||
|
: bool
|
||||||
|
: True when the Pulseaudio sink (output) exists and is usable
|
||||||
|
| source_online
|
||||||
|
: bool
|
||||||
|
: True when the Pulseaudio source (input) exists and is usable
|
||||||
|
| sink_percent
|
||||||
|
: range
|
||||||
|
: Sink (output) volume level in percentage, with min and max as start and end range values
|
||||||
|
| source_percent
|
||||||
|
: range
|
||||||
|
: Source (input) volume level in percentage, with min and max as start and end range values
|
||||||
|
| sink_muted
|
||||||
|
: bool
|
||||||
|
: True if sink is muted, otherwise false
|
||||||
|
| source_muted
|
||||||
|
: bool
|
||||||
|
: True if source is muted, otherwise false
|
||||||
|
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
|
||||||
|
[[ *Name*
|
||||||
|
:[ *Type*
|
||||||
|
:[ *Req*
|
||||||
|
:[ *Description*
|
||||||
|
| sink_name
|
||||||
|
: string
|
||||||
|
: no
|
||||||
|
: Sink name to monitor. Defaults to _@DEFAULT\_SINK@_
|
||||||
|
| source_name
|
||||||
|
: string
|
||||||
|
: no
|
||||||
|
: Source name to monitor. Defaults to _@DEFAULT\_SOURCE@_
|
||||||
|
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
```
|
||||||
|
bar:
|
||||||
|
left:
|
||||||
|
- pulse:
|
||||||
|
content: {string: {text: "{sink_percent} %"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)
|
||||||
|
|
|
@ -226,6 +226,54 @@ bar:
|
||||||
"":
|
"":
|
||||||
- string: {text: , font: *awesome, foreground: ffffff66}
|
- string: {text: , font: *awesome, foreground: ffffff66}
|
||||||
- string: {text: "{ssid}", foreground: ffffff66}
|
- string: {text: "{ssid}", foreground: ffffff66}
|
||||||
|
- pulse:
|
||||||
|
anchors:
|
||||||
|
- string: &common {on-click: "pavucontrol"}
|
||||||
|
- string: &offline {foreground: dc322fff}
|
||||||
|
- string: &muted {foreground: 839496ff}
|
||||||
|
- in: &in
|
||||||
|
- string: {<<: *common, text: "", font: *awesome}
|
||||||
|
- string: {<<: *common, text: "{source_percent}%"}
|
||||||
|
- out: &out
|
||||||
|
- string: {<<: *common, text: "🔊︁", font: *awesome}
|
||||||
|
- string: {<<: *common, text: "{sink_percent}%"}
|
||||||
|
- in_muted: &in_muted
|
||||||
|
- string: {<<: [*common, *muted], text: "", font: *awesome}
|
||||||
|
- string: {<<: [*common, *muted], text: "{source_percent}%"}
|
||||||
|
- out_muted: &out_muted
|
||||||
|
- string: {<<: [*common, *muted], text: "", font: *awesome}
|
||||||
|
- string: {<<: [*common, *muted], text: "{sink_percent}%"}
|
||||||
|
|
||||||
|
content:
|
||||||
|
map:
|
||||||
|
tag: sink_online
|
||||||
|
values:
|
||||||
|
false:
|
||||||
|
- string: {<<: [*offline, *common], text: , font: *awesome}
|
||||||
|
true:
|
||||||
|
map:
|
||||||
|
tag: sink_muted
|
||||||
|
values:
|
||||||
|
true:
|
||||||
|
map:
|
||||||
|
tag: source_muted
|
||||||
|
values:
|
||||||
|
true:
|
||||||
|
- *out_muted
|
||||||
|
- *in_muted
|
||||||
|
false:
|
||||||
|
- *out_muted
|
||||||
|
- *in
|
||||||
|
false:
|
||||||
|
map:
|
||||||
|
tag: source_muted
|
||||||
|
values:
|
||||||
|
true:
|
||||||
|
- *out
|
||||||
|
- *in_muted
|
||||||
|
false:
|
||||||
|
- *out
|
||||||
|
- *in
|
||||||
- alsa:
|
- alsa:
|
||||||
card: hw:PCH
|
card: hw:PCH
|
||||||
mixer: Master
|
mixer: Master
|
||||||
|
|
|
@ -125,7 +125,8 @@ yambar = executable(
|
||||||
version,
|
version,
|
||||||
dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] +
|
dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] +
|
||||||
decorations + particles + modules,
|
decorations + particles + modules,
|
||||||
c_args: [plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[]],
|
c_args: [plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[],
|
||||||
|
plugin_pulse_enabled? '-DPLUGIN_ENABLED_PULSE':[]],
|
||||||
build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles',
|
build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles',
|
||||||
export_dynamic: true,
|
export_dynamic: true,
|
||||||
install: true,
|
install: true,
|
||||||
|
@ -162,7 +163,10 @@ summary(
|
||||||
)
|
)
|
||||||
|
|
||||||
summary(
|
summary(
|
||||||
{'Music Player Daemon (MPD)': plugin_mpd_enabled},
|
{
|
||||||
|
'Music Player Daemon (MPD)': plugin_mpd_enabled,
|
||||||
|
'Pulseaudio': plugin_pulse_enabled,
|
||||||
|
},
|
||||||
section: 'Optional modules',
|
section: 'Optional modules',
|
||||||
bool_yn: true
|
bool_yn: true
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,3 +8,6 @@ option(
|
||||||
option(
|
option(
|
||||||
'plugin-mpd', type: 'feature', value: 'auto',
|
'plugin-mpd', type: 'feature', value: 'auto',
|
||||||
description: 'Music Player Daemon (MPD) support')
|
description: 'Music Player Daemon (MPD) support')
|
||||||
|
option(
|
||||||
|
'plugin-pulse', type: 'feature', value: 'auto',
|
||||||
|
description: 'Pulseaudio support')
|
||||||
|
|
|
@ -11,6 +11,9 @@ xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11'))
|
||||||
mpd = dependency('libmpdclient', required: get_option('plugin-mpd'))
|
mpd = dependency('libmpdclient', required: get_option('plugin-mpd'))
|
||||||
plugin_mpd_enabled = mpd.found()
|
plugin_mpd_enabled = mpd.found()
|
||||||
|
|
||||||
|
pulse = dependency('libpulse', required: get_option('plugin-pulse'))
|
||||||
|
plugin_pulse_enabled = pulse.found()
|
||||||
|
|
||||||
# Module name -> (source-list, dep-list)
|
# Module name -> (source-list, dep-list)
|
||||||
mod_data = {
|
mod_data = {
|
||||||
'alsa': [[], [m, alsa]],
|
'alsa': [[], [m, alsa]],
|
||||||
|
@ -29,6 +32,10 @@ if plugin_mpd_enabled
|
||||||
mod_data += {'mpd': [[], [mpd]]}
|
mod_data += {'mpd': [[], [mpd]]}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if plugin_pulse_enabled
|
||||||
|
mod_data += {'pulse': [[], [pulse]]}
|
||||||
|
endif
|
||||||
|
|
||||||
if backend_x11
|
if backend_x11
|
||||||
mod_data += {
|
mod_data += {
|
||||||
'xkb': [[], [xcb_stuff, xcb_xkb]],
|
'xkb': [[], [xcb_stuff, xcb_xkb]],
|
||||||
|
|
433
modules/pulse.c
Normal file
433
modules/pulse.c
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <pulse/def.h>
|
||||||
|
#include <pulse/introspect.h>
|
||||||
|
#include <pulse/operation.h>
|
||||||
|
#include <pulse/proplist.h>
|
||||||
|
#include <pulse/thread-mainloop.h>
|
||||||
|
#include <pulse/volume.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "pulse/pulseaudio.h"
|
||||||
|
|
||||||
|
#define LOG_MODULE "pulse"
|
||||||
|
#define LOG_ENABLE_DBG 1
|
||||||
|
|
||||||
|
#include "../bar/bar.h"
|
||||||
|
#include "../config-verify.h"
|
||||||
|
#include "../config.h"
|
||||||
|
#include "../log.h"
|
||||||
|
#include "../plugin.h"
|
||||||
|
|
||||||
|
struct private
|
||||||
|
{
|
||||||
|
char *sink_name;
|
||||||
|
char *source_name;
|
||||||
|
struct particle *label;
|
||||||
|
|
||||||
|
bool sink_online;
|
||||||
|
bool sink_muted;
|
||||||
|
pa_volume_t sink_cur_volume;
|
||||||
|
|
||||||
|
bool source_online;
|
||||||
|
bool source_muted;
|
||||||
|
pa_volume_t source_cur_volume;
|
||||||
|
|
||||||
|
bool online;
|
||||||
|
|
||||||
|
uint sink_index;
|
||||||
|
uint source_index;
|
||||||
|
char *current_sink_name;
|
||||||
|
char *current_source_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
destroy(struct module *mod)
|
||||||
|
{
|
||||||
|
struct private *m = mod->private;
|
||||||
|
m->label->destroy(m->label);
|
||||||
|
free(m->current_sink_name);
|
||||||
|
free(m->current_source_name);
|
||||||
|
free(m->sink_name);
|
||||||
|
free(m->source_name);
|
||||||
|
free(m);
|
||||||
|
module_default_destroy(mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
description(struct module *mod)
|
||||||
|
{
|
||||||
|
static char desc[32];
|
||||||
|
snprintf(desc, sizeof(desc), "pulse");
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct exposable *
|
||||||
|
content(struct module *mod)
|
||||||
|
{
|
||||||
|
struct private *p = mod->private;
|
||||||
|
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
|
||||||
|
int sink_percent = (p->sink_cur_volume * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM;
|
||||||
|
int source_percent = (p->source_cur_volume * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM;
|
||||||
|
|
||||||
|
// Note that Pulseaudio source/sink can go over 100% volume
|
||||||
|
struct tag_set tags = {
|
||||||
|
.tags =
|
||||||
|
(struct tag *[]){
|
||||||
|
tag_new_bool(mod, "online", p->online),
|
||||||
|
tag_new_bool(mod, "sink_online", p->sink_online),
|
||||||
|
tag_new_bool(mod, "sink_muted", p->sink_muted),
|
||||||
|
tag_new_int_range(mod, "sink_percent", sink_percent, 0, 100),
|
||||||
|
tag_new_bool(mod, "source_online", p->source_online),
|
||||||
|
tag_new_bool(mod, "source_muted", p->source_muted),
|
||||||
|
tag_new_int_range(mod, "source_percent", source_percent, 0, 100),
|
||||||
|
},
|
||||||
|
.count = 7,
|
||||||
|
};
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
|
||||||
|
struct exposable *exposable = p->label->instantiate(p->label, &tags);
|
||||||
|
|
||||||
|
tag_set_destroy(&tags);
|
||||||
|
return exposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sink_info_callback(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
|
||||||
|
{
|
||||||
|
struct module *mod = (struct module *)userdata;
|
||||||
|
struct private *p = mod->private;
|
||||||
|
if (!i)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Average of the channels
|
||||||
|
pa_volume_t vol = pa_cvolume_avg(&i->volume);
|
||||||
|
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->sink_cur_volume = vol;
|
||||||
|
p->sink_muted = i->mute;
|
||||||
|
p->sink_online = true;
|
||||||
|
p->sink_index = i->index;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
source_info_callback(pa_context *c, const pa_source_info *i, int eol, void *userdata)
|
||||||
|
{
|
||||||
|
struct module *mod = (struct module *)userdata;
|
||||||
|
struct private *p = mod->private;
|
||||||
|
if (!i)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Average of the channels
|
||||||
|
pa_volume_t vol = pa_cvolume_avg(&i->volume);
|
||||||
|
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->source_cur_volume = vol;
|
||||||
|
p->source_online = true;
|
||||||
|
p->source_muted = i->mute;
|
||||||
|
p->source_index = i->index;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
server_info_callback(pa_context *c, const pa_server_info *i, void *userdata)
|
||||||
|
{
|
||||||
|
struct module *mod = (struct module *)userdata;
|
||||||
|
struct private *p = mod->private;
|
||||||
|
pa_operation *o;
|
||||||
|
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
// If they match defaults that means there are no sinks/sources
|
||||||
|
if (strcmp(i->default_sink_name, "@DEFAULT_SINK@") == 0) {
|
||||||
|
p->sink_online = false;
|
||||||
|
p->sink_index = 0;
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
}
|
||||||
|
if (strcmp(i->default_source_name, "@DEFAULT_SOURCE@") == 0) {
|
||||||
|
p->source_online = false;
|
||||||
|
p->source_index = 0;
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->current_sink_name || strcmp(i->default_sink_name, p->current_sink_name) != 0) {
|
||||||
|
LOG_DBG("Default sink changed (%s) - calling get_sink_info_by_name", i->default_sink_name);
|
||||||
|
free(p->current_sink_name);
|
||||||
|
p->current_sink_name = strdup(i->default_sink_name);
|
||||||
|
if (!(o = pa_context_get_sink_info_by_name(c, p->sink_name, sink_info_callback,
|
||||||
|
userdata))) {
|
||||||
|
LOG_ERR("pa_context_get_sink_info_by_name() failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
} else if (!p->current_source_name ||
|
||||||
|
strcmp(i->default_source_name, p->current_source_name) != 0) {
|
||||||
|
LOG_DBG("Default source changed (%s) - calling get_sink_info_by_name",
|
||||||
|
i->default_source_name);
|
||||||
|
free(p->current_source_name);
|
||||||
|
p->current_source_name = strdup(i->default_source_name);
|
||||||
|
if (!(o = pa_context_get_source_info_by_name(c, p->source_name, source_info_callback,
|
||||||
|
userdata))) {
|
||||||
|
LOG_ERR("pa_context_get_source_info_by_name() failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We subscribe to events on sink/sources and server configuration changes
|
||||||
|
|
||||||
|
Sinks and sources so that we can react to volume/mute changes for example.
|
||||||
|
Server change subscription gets triggered when default sink/source changes
|
||||||
|
(among other things) so it lets us immediately react
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
pa_subscription_callback(pa_context *c, pa_subscription_event_type_t t, uint idx, void *userdata)
|
||||||
|
{
|
||||||
|
struct module *mod = (struct module *)userdata;
|
||||||
|
struct private *p = mod->private;
|
||||||
|
pa_operation *o;
|
||||||
|
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
|
||||||
|
case PA_SUBSCRIPTION_EVENT_SINK:
|
||||||
|
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
|
||||||
|
if (idx == p->sink_index) {
|
||||||
|
p->sink_online = false;
|
||||||
|
p->sink_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->sink_index || p->sink_index == idx) {
|
||||||
|
LOG_DBG("calling get_sink_info_by_name: %d", idx);
|
||||||
|
if (!(o = pa_context_get_sink_info_by_name(c, p->sink_name, sink_info_callback,
|
||||||
|
userdata))) {
|
||||||
|
LOG_ERR("pa_context_get_sink_info_by_name() failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PA_SUBSCRIPTION_EVENT_SOURCE:
|
||||||
|
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
|
||||||
|
if (idx == p->source_index) {
|
||||||
|
p->source_online = false;
|
||||||
|
p->source_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->source_index || p->source_index == idx) {
|
||||||
|
LOG_DBG("calling get_source_info_by_name: %d", idx);
|
||||||
|
if (!(o = pa_context_get_source_info_by_name(c, p->source_name, source_info_callback,
|
||||||
|
userdata))) {
|
||||||
|
LOG_ERR("pa_context_get_source_info_by_name() failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
}
|
||||||
|
case PA_SUBSCRIPTION_EVENT_SERVER:
|
||||||
|
// Update when server state changes - for example changes in default sink/source
|
||||||
|
if (!(o = pa_context_get_server_info(c, server_info_callback, userdata))) {
|
||||||
|
LOG_ERR("pa_context_get_server_info() failed: %s", pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This is called whenever the context status changes - we connect/disconnect etc */
|
||||||
|
static void
|
||||||
|
context_state_callback(pa_context *c, void *userdata)
|
||||||
|
{
|
||||||
|
assert(c);
|
||||||
|
struct module *mod = (struct module *)userdata;
|
||||||
|
struct private *p = mod->private;
|
||||||
|
LOG_DBG("Context state callback called");
|
||||||
|
|
||||||
|
switch (pa_context_get_state(c)) {
|
||||||
|
case PA_CONTEXT_CONNECTING:
|
||||||
|
case PA_CONTEXT_AUTHORIZING:
|
||||||
|
case PA_CONTEXT_SETTING_NAME:
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->online = false;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PA_CONTEXT_READY: {
|
||||||
|
pa_operation *o;
|
||||||
|
assert(c);
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->online = true;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
LOG_DBG("pulse connection established.");
|
||||||
|
pa_context_set_subscribe_callback(c, pa_subscription_callback, mod);
|
||||||
|
if (!(o = pa_context_subscribe(c,
|
||||||
|
PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE |
|
||||||
|
PA_SUBSCRIPTION_MASK_SERVER,
|
||||||
|
NULL, mod))) {
|
||||||
|
LOG_ERR("pa_context_subscribe failed: %s", pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(o = pa_context_get_sink_info_by_name(c, p->sink_name, sink_info_callback, mod))) {
|
||||||
|
LOG_ERR("pa_context_get_sink_info_by_name failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
if (!(o = pa_context_get_source_info_by_name(c, p->source_name, source_info_callback,
|
||||||
|
mod))) {
|
||||||
|
LOG_ERR("pa_context_get_source_info_by_name failed: %s",
|
||||||
|
pa_strerror(pa_context_errno(c)));
|
||||||
|
}
|
||||||
|
pa_operation_unref(o);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PA_CONTEXT_TERMINATED:
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->online = false;
|
||||||
|
p->sink_online = false;
|
||||||
|
p->source_online = false;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
LOG_INFO("pulse connection terminated.");
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
// TODO - possibly schedule a recurring reconnect attempt?
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PA_CONTEXT_FAILED:
|
||||||
|
default:
|
||||||
|
mtx_lock(&mod->lock);
|
||||||
|
p->online = false;
|
||||||
|
p->sink_online = false;
|
||||||
|
p->source_online = false;
|
||||||
|
mtx_unlock(&mod->lock);
|
||||||
|
LOG_ERR("pulse connection failure: %s", pa_strerror(pa_context_errno(c)));
|
||||||
|
mod->bar->refresh(mod->bar);
|
||||||
|
// TODO - possibly schedule a recurring reconnect attempt?
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
run(struct module *mod)
|
||||||
|
{
|
||||||
|
int ret = 1;
|
||||||
|
pa_threaded_mainloop *m = NULL;
|
||||||
|
pa_context *context = NULL;
|
||||||
|
pa_mainloop_api *mainloop_api = NULL;
|
||||||
|
|
||||||
|
if (!(m = pa_threaded_mainloop_new())) {
|
||||||
|
LOG_ERR("pa_mainloop_new() failed");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainloop_api = pa_threaded_mainloop_get_api(m);
|
||||||
|
|
||||||
|
if (!(context = pa_context_new(mainloop_api, NULL))) {
|
||||||
|
LOG_ERR("pa_context_new() failed.");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
pa_context_set_state_callback(context, context_state_callback, mod);
|
||||||
|
|
||||||
|
/* Connect the context */
|
||||||
|
if (pa_context_connect(context, NULL, 0, NULL) < 0) {
|
||||||
|
LOG_ERR("pa_context_connect() failed: %s\n", pa_strerror(pa_context_errno(context)));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Let's start a separate thread for handling all pulseaudio events */
|
||||||
|
pa_threaded_mainloop_start(m);
|
||||||
|
|
||||||
|
/* And wait for termination event from abort_fd */
|
||||||
|
while (true) {
|
||||||
|
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
|
||||||
|
int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
LOG_ERRNO("failed to poll");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fds[0].revents & (POLLIN | POLLHUP)) {
|
||||||
|
// This is the only way to exit without an error
|
||||||
|
ret = 0;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
if (m) {
|
||||||
|
pa_threaded_mainloop_stop(m);
|
||||||
|
pa_threaded_mainloop_free(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct module *
|
||||||
|
pulse_new(const char *sink_name, const char *source_name, struct particle *label)
|
||||||
|
{
|
||||||
|
struct private *priv = calloc(1, sizeof(*priv));
|
||||||
|
priv->label = label;
|
||||||
|
priv->sink_name = strdup(sink_name);
|
||||||
|
priv->source_name = strdup(source_name);
|
||||||
|
priv->source_online = false;
|
||||||
|
priv->sink_online = false;
|
||||||
|
priv->online = false;
|
||||||
|
|
||||||
|
struct module *mod = module_common_new();
|
||||||
|
mod->private = priv;
|
||||||
|
mod->run = &run;
|
||||||
|
mod->destroy = &destroy;
|
||||||
|
mod->content = &content;
|
||||||
|
mod->description = &description;
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct module *
|
||||||
|
from_conf(const struct yml_node *node, struct conf_inherit inherited)
|
||||||
|
{
|
||||||
|
const struct yml_node *sink_name = yml_get_value(node, "sink_name");
|
||||||
|
const struct yml_node *source_name = yml_get_value(node, "source_name");
|
||||||
|
const struct yml_node *content = yml_get_value(node, "content");
|
||||||
|
|
||||||
|
return pulse_new(sink_name != NULL ? yml_value_as_string(sink_name) : "@DEFAULT_SINK@",
|
||||||
|
source_name != NULL ? yml_value_as_string(source_name) : "@DEFAULT_SOURCE@",
|
||||||
|
conf_to_particle(content, inherited));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
verify_conf(keychain_t *chain, const struct yml_node *node)
|
||||||
|
{
|
||||||
|
static const struct attr_info attrs[] = {
|
||||||
|
{"sink_name", false, &conf_verify_string},
|
||||||
|
{"source_name", false, &conf_verify_string},
|
||||||
|
MODULE_COMMON_ATTRS,
|
||||||
|
};
|
||||||
|
|
||||||
|
return conf_verify_dict(chain, node, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct module_iface module_pulse_iface = {
|
||||||
|
.verify_conf = &verify_conf,
|
||||||
|
.from_conf = &from_conf,
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
|
||||||
|
extern const struct module_iface iface __attribute__((weak, alias("module_pulse_iface")));
|
||||||
|
#endif
|
6
plugin.c
6
plugin.c
|
@ -43,6 +43,9 @@ EXTERN_MODULE(label);
|
||||||
EXTERN_MODULE(mpd);
|
EXTERN_MODULE(mpd);
|
||||||
#endif
|
#endif
|
||||||
EXTERN_MODULE(network);
|
EXTERN_MODULE(network);
|
||||||
|
#if defined(PLUGIN_ENABLED_PULSE)
|
||||||
|
EXTERN_MODULE(pulse);
|
||||||
|
#endif
|
||||||
EXTERN_MODULE(removables);
|
EXTERN_MODULE(removables);
|
||||||
EXTERN_MODULE(river);
|
EXTERN_MODULE(river);
|
||||||
EXTERN_MODULE(sway_xkb);
|
EXTERN_MODULE(sway_xkb);
|
||||||
|
@ -123,6 +126,9 @@ init(void)
|
||||||
REGISTER_CORE_MODULE(mpd, mpd);
|
REGISTER_CORE_MODULE(mpd, mpd);
|
||||||
#endif
|
#endif
|
||||||
REGISTER_CORE_MODULE(network, network);
|
REGISTER_CORE_MODULE(network, network);
|
||||||
|
#if defined(PLUGIN_ENABLED_PULSE)
|
||||||
|
REGISTER_CORE_MODULE(pulse, pulse);
|
||||||
|
#endif
|
||||||
REGISTER_CORE_MODULE(removables, removables);
|
REGISTER_CORE_MODULE(removables, removables);
|
||||||
#if defined(HAVE_PLUGIN_river)
|
#if defined(HAVE_PLUGIN_river)
|
||||||
REGISTER_CORE_MODULE(river, river);
|
REGISTER_CORE_MODULE(river, river);
|
||||||
|
|
Loading…
Add table
Reference in a new issue