/*****************************************************************************
* pipewire.c: PipeWire audio output plugin for VLC
*****************************************************************************
* Copyright (C) 2022 Rémi Denis-Courmont
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <search.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <pipewire/pipewire.h>
#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_aout.h>
#include <vlc_tick.h>
#include "vlc_pipewire.h"
struct vlc_pw_stream {
struct vlc_pw_context *context;
struct pw_stream *stream;
struct spa_hook listener;
size_t stride;
struct {
vlc_frame_t *head;
vlc_frame_t **tailp;
size_t size;
} queue;
struct {
vlc_tick_t pts;
ptrdiff_t frames;
unsigned int rate;
uint64_t injected;
vlc_tick_t next_update;
} time;
vlc_tick_t start;
vlc_tick_t first_pts;
bool starting;
bool draining;
bool error;
audio_output_t *aout;
};
/**
* Stream control callback.
*
* This monitors the stream for control changes and reports them as applicable.
*/
static void stream_control_info(void *data, uint32_t id,
const struct pw_stream_control *control)
{
struct vlc_pw_stream *s = data;
vlc_pw_debug(s->context, "control %"PRIu32" %s", id, control->name);
switch (id) {
case SPA_PROP_mute:
aout_MuteReport(s->aout, control->values[0] != 0.f);
break;
case SPA_PROP_channelVolumes: {
float vol = 0.f;
for (size_t i = 0; i < control->n_values; i++)
vol = fmaxf(vol, control->values[i]);
/* Cubic root as the reciprocal mapping of set_volume()'s */
aout_VolumeReport(s->aout, cbrtf(vol));
break;
}
}
}
/**
* Stream state callback.
*
* This monitors the stream for state change, looking out for fatal errors.
*/
static void stream_state_changed(void *data, enum pw_stream_state old,
enum pw_stream_state state, const char *err)
{
struct vlc_pw_stream *s = data;
if (state == PW_STREAM_STATE_ERROR)
{
vlc_pw_error(s->context, "stream error: %s", err);
s->error = true;
}
else
vlc_pw_debug(s->context, "stream %s",
pw_stream_state_as_string(state));
if (old != state)
vlc_pw_signal(s->context);
}
/**
* Retrieve latest timings
*/
static int stream_update_latency(struct vlc_pw_stream *s, vlc_tick_t *now)
{
struct pw_time ts;
#if PW_CHECK_VERSION(1, 1, 0)
/* PW monotonic clock, same than vlc_tick_now() */
*now = VLC_TICK_FROM_NS(pw_stream_get_nsec(s->stream));
#else
*now = vlc_tick_now();
#endif
if (pw_stream_get_time_n(s->stream, &ts, sizeof (ts)) < 0
|| ts.rate.denom == 0)
return -1;
s->time.pts = VLC_TICK_FROM_NS(ts.now);
s->time.pts += vlc_tick_from_frac(ts.delay * ts.rate.num, ts.rate.denom);
s->time.frames = ts.buffered + ts.queued;
return 0;
}
/**
* Stream processing callback.
*
* This fills in the next audio buffer.
*/
static void stream_process(void *data)
{
struct vlc_pw_stream *s = data;
vlc_tick_t now;
int val = stream_update_latency(s, &now);
struct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);
if (likely(b != NULL)) {
/* One should have more indirection layers than pizza condiments */
struct spa_buffer *buf = b->buffer;
struct spa_data *d = &buf->datas[0];
struct spa_chunk *chunk = d->chunk;
unsigned char *dst = d->data;
size_t frame_room = d->maxsize / s->stride;
size_t room = frame_room * s->stride;
vlc_frame_t *block;
chunk->offset = 0;
chunk->stride = s->stride;
chunk->size = 0;
/* Adjust start time */
if (s->starting) {
/*
* If timing data is not available (val != 0), we can at least
* assume that the playback delay is no less than zero.
*/
vlc_tick_t pts = (val == 0) ? s->time.pts : now;
vlc_tick_t gap = s->start - pts;
vlc_tick_t span = vlc_tick_from_samples(frame_room, s->time.rate);
size_t skip;
if (gap >= span) { /* Buffer too early, fill with silence */
vlc_pw_debug(s->context, "too early to start, silence");
skip = frame_room;
} else if (gap >= 0) {
vlc_pw_debug(s->context, "starting %s time",
val ? "without" : "on");
skip = samples_from_vlc_tick(gap, s->time.rate);
s->starting = false;
} else {
vlc_pw_warn(s->context, "starting late");
skip = 0;
s->starting = false;
}
skip *= s->stride;
assert(skip <= room);
memset(dst, 0, skip);
dst += skip;
room -= skip;
}
if (!s->starting && s->time.pts != VLC_TICK_INVALID
&& now >= s->time.next_update && s->first_pts != VLC_TICK_INVALID) {
vlc_tick_t elapsed = now - s->time.pts;
/* A sample injected now would be delayed by the following amount of vlc_tick_t */
vlc_tick_t delay = vlc_tick_from_samples(s->time.frames, s->time.rate)
- elapsed;
vlc_tick_t audio_ts = vlc_tick_from_samples(s->time.injected, s->time.rate)
+ s->first_pts;
aout_TimingReport(s->aout, now + delay, audio_ts);
/* Once we have enough points to initiate the clock we can delay the reports */
if (now >= s->start + VLC_TICK_FROM_SEC(1))
s->time.next_update = now + VLC_TICK_FROM_SEC(1);
}
while ((block = s->queue.head) != NULL) {
size_t avail = block->i_buffer;
size_t length = (avail < room) ? avail : room;
memcpy(dst, block->p_buffer, length);
block->p_buffer += length;
block->i_buffer -= length;
dst += length;
room -= length;
chunk->size += length;
assert((length % s->stride) == 0);
const size_t written = length / s->stride;
s->time.injected += written;
s->queue.size -= length;
if (block->i_buffer > 0) {
assert(room == 0);
break;
}
s->queue.head = block->p_next;
vlc_frame_Release(block);
}
if (s->queue.head == NULL)
s->queue.tailp = &s->queue.head;
b->size = chunk->size / s->stride;
pw_stream_queue_buffer(s->stream, b);
}
if (s->queue.head == NULL && s->draining) {
s->first_pts = s->start = VLC_TICK_INVALID;
s->starting = false;
s->draining = false;
pw_stream_flush(s->stream, true);
}
}
/**
* Stream drain callback.
*
* This monitors the stream for completion of draining.
*/
static void stream_drained(void *data)
{
struct vlc_pw_stream *s = data;
vlc_pw_debug(s->context, "stream drained");
aout_DrainedReport(s->aout);
}
static void stream_trigger_done(void *data)
{
struct vlc_pw_stream *s = data;
vlc_pw_debug(s->context, "stream trigger done");
}
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.state_changed = stream_state_changed,
.control_info = stream_control_info,
.process = stream_process,
.drained = stream_drained,
.trigger_done = stream_trigger_done,
};
static enum pw_stream_state vlc_pw_stream_get_state(struct vlc_pw_stream *s)
{
/* PW Workaround: The error state can be notified via
* stream_state_changed() but never returned by pw_stream_get_state() */
return s->error ? PW_STREAM_STATE_ERROR
: pw_stream_get_state(s->stream, NULL);
}
/**
* Queues an audio buffer for playback.
*/
static void vlc_pw_stream_play(struct vlc_pw_stream *s, vlc_frame_t *block,
vlc_tick_t date)
{
assert((block->i_buffer % s->stride) == 0);
vlc_pw_lock(s->context);
if (vlc_pw_stream_get_state(s) == PW_STREAM_STATE_ERROR) {
vlc_frame_Release(block);
goto out;
}
if (s->start == VLC_TICK_INVALID) {
/* Upon flush or drain, the stream is implicitly inactivated. This
* re-activates it. In other cases, this should be a no-op. */
pw_stream_set_active(s->stream, true);
assert(!s->starting);
s->starting = true;
s->time.next_update = VLC_TICK_0;
s->first_pts = block->i_pts;
}
if (s->starting)
s->start = date
- vlc_tick_from_samples(s->queue.size / s->stride, s->time.rate);
*(s->queue.tailp) = block;
s->queue.tailp = &block->p_next;
s->queue.size += block->i_buffer;
out:
s->draining = false;
vlc_pw_unlock(s->context);
}
/**
* Pauses or resumes the playback.
*/
static void vlc_pw_stream_set_pause(struct vlc_pw_stream *s, bool paused,
vlc_tick_t date)
{
vlc_pw_lock(s->context);
pw_stream_set_active(s->stream, !paused);
s->time.pts = VLC_TICK_INVALID;
if (unlikely(s->starting)) {
assert(s->start != VLC_TICK_INVALID);
if (paused)
s->start -= date;
else
s->start += date;
}
vlc_pw_unlock(s->context);
}
/**
* Flushes (discards) all pending audio buffers.
*/
static void vlc_pw_stream_flush(struct vlc_pw_stream *s)
{
vlc_pw_lock(s->context);
vlc_frame_ChainRelease(s->queue.head);
s->queue.head = NULL;
s->queue.tailp = &s->queue.head;
s->queue.size = 0;
s->time.pts = VLC_TICK_INVALID;
s->time.injected = 0;
s->first_pts = s->start = VLC_TICK_INVALID;
s->starting = false;
s->draining = false;
s->error = false;
pw_stream_flush(s->stream, false);
vlc_pw_unlock(s->context);
}
/**
* Starts draining.
*
* This flags the start of draining. stream_drained() will be called by the
* thread loop whence draining completes.
*/
static void vlc_pw_stream_drain(struct vlc_pw_stream *s)
{
vlc_pw_lock(s->context);
bool empty = s->start == VLC_TICK_INVALID;
s->first_pts = s->start = VLC_TICK_INVALID;
if (vlc_pw_stream_get_state(s) == PW_STREAM_STATE_ERROR || empty)
stream_drained(s); /* Don't wait on a failed stream */
else if (s->queue.head == NULL)
pw_stream_flush(s->stream, true); /* Drain now */
else
s->draining = true; /* Let ->process() drain */
vlc_pw_unlock(s->context);
}
static void vlc_pw_stream_set_volume(struct vlc_pw_stream *s, float vol)
{
const struct pw_stream_control *old;
vol = vol * vol * vol; /* same cubic mapping as VLC's software volume */
vlc_pw_lock(s->context);
old = pw_stream_get_control(s->stream, SPA_PROP_channelVolumes);
if (old != NULL) {
float values[SPA_AUDIO_MAX_CHANNELS];
float oldvol = 0.f;
assert(old->n_values <= ARRAY_SIZE(values));
/* Try to preserve the balance */
for (size_t i = 0; i < old->n_values; i++)
oldvol = fmaxf(oldvol, old->values[i]);
float delta = vol - oldvol;
for (size_t i = 0; i < old->n_values; i++)
values[i] = fmaxf(0.f, old->values[i] + delta);
pw_stream_set_control(s->stream, SPA_PROP_channelVolumes,
old->n_values, values, 0);
}
vlc_pw_unlock(s->context);
}
static void vlc_pw_stream_set_mute(struct vlc_pw_stream *s, bool mute)
{
float value = mute ? 1.f : 0.f;
vlc_pw_lock(s->context);
pw_stream_set_control(s->stream, SPA_PROP_mute, 1, &value, 0);
vlc_pw_unlock(s->context);
}
static void vlc_pw_stream_select_device(struct vlc_pw_stream *s,
const char *name)
{
struct spa_dict_item items[] = {
SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, name),
};
struct spa_dict dict = SPA_DICT_INIT(items, ARRAY_SIZE(items));
vlc_pw_debug(s->context, "setting object serial: %s", name);
vlc_pw_lock(s->context);
pw_stream_update_properties(s->stream, &dict);
vlc_pw_unlock(s->context);
}
static void vlc_pw_stream_destroy(struct vlc_pw_stream *s)
{
vlc_pw_stream_flush(s);
vlc_pw_lock(s->context);
pw_stream_disconnect(s->stream);
pw_stream_destroy(s->stream);
vlc_pw_unlock(s->context);
free(s);
}
struct vlc_pw_aout {
struct vlc_pw_context *context;
struct vlc_pw_stream *stream;
struct spa_hook listener;
void *nodes;
struct {
uint64_t target;
float volume;
signed char mute;
} initial;
};
static struct vlc_pw_stream *vlc_pw_stream_create(audio_output_t *aout,
audio_sample_format_t *restrict fmt)
{
struct vlc_pw_aout *sys = aout->sys;
struct vlc_pw_context *ctx = sys->context;
struct spa_audio_info_raw rawfmt = {
.rate = fmt->i_rate,
.channels = fmt->i_channels,
};
enum spa_audio_iec958_codec encoding = SPA_AUDIO_IEC958_CODEC_PCM; /* PCM by default */
/* Map possible audio output formats */
switch (fmt->i_format) {
case VLC_CODEC_FL64:
rawfmt.format = SPA_AUDIO_FORMAT_F64;
break;
case VLC_CODEC_FL32:
rawfmt.format = SPA_AUDIO_FORMAT_F32;
break;
case VLC_CODEC_S32N:
rawfmt.format = SPA_AUDIO_FORMAT_S32;
break;
case VLC_CODEC_S16N:
rawfmt.format = SPA_AUDIO_FORMAT_S16;
break;
case VLC_CODEC_U8:
rawfmt.format = SPA_AUDIO_FORMAT_U8;
break;
case VLC_CODEC_A52:
fmt->i_format = VLC_CODEC_SPDIFL;
fmt->i_bytes_per_frame = 4;
fmt->i_frame_length = 1;
fmt->i_physical_channels = AOUT_CHANS_2_0;
fmt->i_channels = 2;
encoding = SPA_AUDIO_IEC958_CODEC_AC3;
break;
case VLC_CODEC_EAC3:
fmt->i_format = VLC_CODEC_SPDIFL;
fmt->i_bytes_per_frame = 4;
fmt->i_frame_length = 1;
fmt->i_physical_channels = AOUT_CHANS_2_0;
fmt->i_channels = 2;
encoding = SPA_AUDIO_IEC958_CODEC_EAC3;
break;
case VLC_CODEC_DTS:
fmt->i_format = VLC_CODEC_SPDIFL;
fmt->i_bytes_per_frame = 4;
fmt->i_frame_length = 1;
fmt->i_physical_channels = AOUT_CHANS_2_0;
fmt->i_channels = 2;
encoding = SPA_AUDIO_IEC958_CODEC_DTS;
break;
default:
vlc_pw_error(sys->context, "unknown format");
errno = ENOTSUP;
return NULL;
}
/* Map audio channels */
if (encoding == SPA_AUDIO_IEC958_CODEC_PCM)
{
size_t mapped = 0;
if (fmt->i_channels > SPA_AUDIO_MAX_CHANNELS) {
vlc_pw_error(sys->context, "too many channels");
errno = ENOTSUP;
return NULL;
}
// NOTE: MUST match VLC (WG4) order
static const unsigned char map[] = {
SPA_AUDIO_CHANNEL_FL,
SPA_AUDIO_CHANNEL_FR,
SPA_AUDIO_CHANNEL_SL,
SPA_AUDIO_CHANNEL_SR,
SPA_AUDIO_CHANNEL_RL,
SPA_AUDIO_CHANNEL_RR,
SPA_AUDIO_CHANNEL_RC,
SPA_AUDIO_CHANNEL_FC,
SPA_AUDIO_CHANNEL_LFE,
};
static_assert (ARRAY_SIZE(map) <= ARRAY_SIZE(pi_vlc_chan_order_wg4),
"Array size mismatch");
for (size_t i = 0; i < ARRAY_SIZE(map); i++)
if (fmt->i_physical_channels & pi_vlc_chan_order_wg4[i])
rawfmt.position[mapped++] = map[i];
while (mapped < fmt->i_channels) {
rawfmt.position[mapped] = SPA_AUDIO_CHANNEL_START_Aux + mapped;
mapped++;
}
}
/* Assemble the stream format */
const struct spa_pod *params[1];
unsigned char buf[1024];
struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof (buf));
if (encoding == SPA_AUDIO_IEC958_CODEC_PCM)
{
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat,
&rawfmt);
}
else /* pass-through */
{
struct spa_audio_info_iec958 iecfmt = {
.rate = fmt->i_rate,
.codec = encoding
};
params[0] = spa_format_audio_iec958_build(&builder, SPA_PARAM_EnumFormat,
&iecfmt);
}
/* Assemble stream properties */
struct pw_properties *props;
props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
NULL);
if (unlikely(props == NULL))
return NULL;
char *role = var_InheritString(aout, "role");
if (role != NULL) { /* Capitalise the first character */
pw_properties_setf(props, PW_KEY_MEDIA_ROLE, "%c%s",
toupper((unsigned char)role[0]), role + 1);
free(role);
}
if (fmt->channel_type == AUDIO_CHANNEL_TYPE_AMBISONICS)
{
fmt->channel_type = AUDIO_CHANNEL_TYPE_BITMAP;
/* Setup low latency in order to quickly react to ambisonics
* filters viewpoint changes. */
pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%"PRId64"/%u",
samples_from_vlc_tick(3 * AOUT_MIN_PREPARE_TIME,
fmt->i_rate),
fmt->i_rate);
}
/* Create the stream */
struct vlc_pw_stream *s = malloc(sizeof (*s));
if (unlikely(s == NULL)) {
pw_properties_free(props);
return NULL;
}
enum pw_stream_flags flags =
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS;
/*PW_STREAM_FLAG_EXCLUSIVE TODO*/
enum pw_stream_state state;
s->context = ctx;
s->listener = (struct spa_hook){ };
s->stride = fmt->i_bytes_per_frame;
s->queue.head = NULL;
s->queue.tailp = &s->queue.head;
s->queue.size = 0;
s->time.pts = VLC_TICK_INVALID;
s->time.rate = fmt->i_rate;
s->time.injected = 0;
s->first_pts = s->start = VLC_TICK_INVALID;
s->starting = false;
s->draining = false;
s->error = false;
s->aout = aout;
vlc_pw_lock(s->context);
s->stream = vlc_pw_stream_new(s->context, "audio stream", props);
if (unlikely(s->stream == NULL)) {
vlc_pw_unlock(s->context);
free(s);
return NULL;
}
if (sys->initial.target != SPA_ID_INVALID) {
char target[21];
struct spa_dict_item items[] = {
SPA_DICT_ITEM_INIT(PW_KEY_TARGET_OBJECT, target),
};
struct spa_dict dict = SPA_DICT_INIT(items, ARRAY_SIZE(items));
snprintf(target, sizeof (target), "%"PRIu64, sys->initial.target);
pw_stream_update_properties(s->stream, &dict);
}
pw_stream_add_listener(s->stream, &s->listener, &stream_events, s);
pw_stream_connect(s->stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, flags,
params, ARRAY_SIZE(params));
if (encoding == SPA_AUDIO_IEC958_CODEC_PCM)
{
/* Wait for the PCM stream to be ready */
while ((state = vlc_pw_stream_get_state(s)) == PW_STREAM_STATE_CONNECTING)
vlc_pw_wait(s->context);
}
else
{
/* In case of passthrough, an error can be triggered just after the
* CONNECTING -> PAUSED state, so wait for the STREAMING state or any
* errors. */
while ((state = vlc_pw_stream_get_state(s)) == PW_STREAM_STATE_CONNECTING
|| state == PW_STREAM_STATE_PAUSED)
vlc_pw_wait(s->context);
}
vlc_pw_unlock(s->context);
switch (state) {
case PW_STREAM_STATE_PAUSED:
case PW_STREAM_STATE_STREAMING:
break;
default:
vlc_pw_stream_destroy(s);
errno = ENOBUFS;
return NULL;
}
/* Apply initial controls */
sys->initial.target = SPA_ID_INVALID;
if (sys->initial.mute >= 0) {
vlc_pw_stream_set_mute(s, sys->initial.mute);
sys->initial.mute = -1;
}
if (!isnan(sys->initial.volume)) {
vlc_pw_stream_set_volume(s, sys->initial.volume);
sys->initial.volume = NAN;
}
return s;
}
static void Play(audio_output_t *aout, vlc_frame_t *block, vlc_tick_t date)
{
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_stream_play(sys->stream, block, date);
}
static void Pause(audio_output_t *aout, bool paused, vlc_tick_t date)
{
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_stream_set_pause(sys->stream, paused, date);
}
static void Flush(audio_output_t *aout)
{
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_stream_flush(sys->stream);
}
static void Drain(audio_output_t *aout)
{
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_stream_drain(sys->stream);
}
static void Stop(audio_output_t *aout)
{
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_stream_destroy(sys->stream);
sys->stream = NULL;
}
static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
{
struct vlc_pw_aout *sys = aout->sys;
sys->stream = vlc_pw_stream_create(aout, fmt);
return (sys->stream != NULL) ? VLC_SUCCESS : -errno;
}
static int VolumeSet(audio_output_t *aout, float volume)
{
struct vlc_pw_aout *sys = aout->sys;
if (sys->stream != NULL) {
vlc_pw_stream_set_volume(sys->stream, volume);
return 0;
}
sys->initial.volume = volume;
aout_VolumeReport(aout, volume);
return 0;
}
static int MuteSet(audio_output_t *aout, bool mute)
{
struct vlc_pw_aout *sys = aout->sys;
if (sys->stream != NULL) {
vlc_pw_stream_set_mute(sys->stream, mute);
return 0;
}
sys->initial.mute = mute;
aout_MuteReport(aout, mute);
return 0;
}
struct vlc_pw_node {
uint32_t id;
uint64_t serial;
};
static int node_by_id(const void *a, const void *b)
{
const struct vlc_pw_node *na = a, *nb = b;
if (na->id > nb->id)
return +1;
if (na->id < nb->id)
return -1;
return 0;
}
static int DeviceSelect(audio_output_t *aout, const char *name)
{
struct vlc_pw_aout *sys = aout->sys;
if (sys->stream != NULL) {
vlc_pw_stream_select_device(sys->stream, name);
return 0;
}
sys->initial.target = strtoul(name, NULL, 10);
aout_DeviceReport(aout, name);
return 0;
}
static void registry_node(audio_output_t *aout, uint32_t id, uint32_t perms,
uint32_t version, const struct spa_dict *props)
{
struct vlc_pw_aout *sys = aout->sys;
char serialstr[21];
uint64_t serial;
if (props == NULL)
return;
const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
if (class == NULL)
return;
if (strcmp(class, "Audio/Sink") && strcmp(class, "Audio/Duplex"))
return; /* Not an audio output */
if (!spa_atou64(spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL), &serial, 0))
return;
const char *desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
if (unlikely(desc == NULL))
desc = "???";
struct vlc_pw_node *node = malloc(sizeof (*node));
if (unlikely(node == NULL))
return;
node->id = id;
node->serial = serial;
struct vlc_pw_node **pp = tsearch(node, &sys->nodes, node_by_id);
if (unlikely(pp == NULL)) { /* Memory allocation error in the tree */
free(node);
return;
}
if (*pp != node) { /* Existing node, update it */
free(*pp);
*pp = node;
}
snprintf(serialstr, sizeof (serialstr), "%"PRIu64, serial);
aout_HotplugReport(aout, serialstr, desc);
(void) perms; (void) version;
}
/**
* Global callback.
*
* This gets called for every initial, then every new, object from the PipeWire
* server. We can find the usable playback devices through this.
*/
static void registry_global(void *data, uint32_t id, uint32_t perms,
const char *name, uint32_t version,
const struct spa_dict *props)
{
audio_output_t *aout = data;
if (strcmp(name, PW_TYPE_INTERFACE_Node) == 0)
registry_node(aout, id, perms, version, props);
}
/**
* Global removal callback.
*
* This gets called when an object disappers. We can detect when a playback
* device is unplugged here.
*/
static void registry_global_remove(void *data, uint32_t id)
{
audio_output_t *aout = data;
struct vlc_pw_aout *sys = aout->sys;
struct vlc_pw_node key = { .id = id };
struct vlc_pw_node **pp = tfind(&key, &sys->nodes, node_by_id);
if (pp != NULL) { /* A known device has been removed */
struct vlc_pw_node *node = *pp;
char serialstr[11];
snprintf(serialstr, sizeof (serialstr), "%"PRIu64, node->serial);
aout_HotplugReport(aout, serialstr, NULL);
tdelete(node, &sys->nodes, node_by_id);
free(node);
}
}
static const struct pw_registry_events events = {
PW_VERSION_REGISTRY_EVENTS,
.global = registry_global,
.global_remove = registry_global_remove
};
static void Close(vlc_object_t *obj)
{
audio_output_t *aout = (audio_output_t *)obj;
struct vlc_pw_aout *sys = aout->sys;
vlc_pw_disconnect(sys->context);
tdestroy(sys->nodes, free);
free(sys);
}
static int Open(vlc_object_t *obj)
{
audio_output_t *aout = (audio_output_t *)obj;
struct vlc_pw_aout *sys = malloc(sizeof (*sys));
if (unlikely(sys == NULL))
return VLC_ENOMEM;
sys->context = vlc_pw_connect(obj, "audio output");
if (sys->context == NULL) {
free(sys);
return -errno;
}
sys->initial.target = SPA_ID_INVALID;
sys->initial.volume = NAN;
sys->initial.mute = -1;
sys->nodes = NULL;
sys->stream = NULL;
aout->sys = sys;
aout->start = Start;
aout->stop = Stop;
aout->play = Play;
aout->pause = Pause;
aout->flush = Flush;
aout->drain = Drain;
aout->volume_set = VolumeSet;
aout->mute_set = MuteSet;
aout->device_select = DeviceSelect;
vlc_pw_lock(sys->context);
vlc_pw_registry_listen(sys->context, &sys->listener, &events, aout);
vlc_pw_roundtrip_unlocked(sys->context); /* Enumerate device nodes */
vlc_pw_unlock(sys->context);
return VLC_SUCCESS;
}
vlc_module_begin()
set_shortname("PipeWire")
set_description(N_("PipeWire audio output"))
set_capability("audio output", 200)
set_subcategory(SUBCAT_AUDIO_AOUT)
add_shortcut("pipewire", "pw")
set_callbacks(Open, Close)
vlc_module_end()
¤ Dauer der Verarbeitung: 0.15 Sekunden
¤
*© Formatika GbR, Deutschland
|
|