mirror of
https://github.com/videolan/vlc.git
synced 2024-11-23 09:53:43 +08:00
pipewire: initial audio output module
This commit is contained in:
parent
9ec7e06a88
commit
f1ab721b0f
1
NEWS
1
NEWS
@ -32,6 +32,7 @@ Core:
|
||||
* Support of HTML help (via the vlc_plugin.h:set_help_html macro)
|
||||
|
||||
Audio output:
|
||||
* PipeWire (native) audio output support
|
||||
* ALSA: HDMI passthrough support.
|
||||
Use --alsa-passthrough to configure S/PDIF or HDMI passthrough.
|
||||
* Remove the DirectSound plugin (API obsolete after Windows 7)
|
||||
|
22
configure.ac
22
configure.ac
@ -3738,6 +3738,28 @@ dnl
|
||||
|
||||
EXTEND_HELP_STRING([Audio plugins:])
|
||||
|
||||
dnl
|
||||
dnl Pipewire module
|
||||
dnl
|
||||
AC_ARG_ENABLE([pipewire],
|
||||
AS_HELP_STRING([--enable-pipewire],
|
||||
[use the PipeWire client library (default auto)]))
|
||||
have_pipewire="no"
|
||||
AS_IF([test "${enable_pipewire}" != "no"], [
|
||||
for v in "0.3 >= 0.3.64" # list of supported package versions
|
||||
do
|
||||
AS_IF([test "${have_pipewire}" = "no"], [
|
||||
PKG_CHECK_MODULES([PIPEWIRE], [libpipewire-${v}], [
|
||||
have_pipewire="yes"
|
||||
], [true])
|
||||
])
|
||||
done
|
||||
AS_IF([test "${enable_pipewire}" = "yes" -a -n "${enable_pipewire}"], [
|
||||
AC_MSG_ERROR([$PIPEWIRE_PKG_ERRORS. Pipewire required.])
|
||||
])
|
||||
])
|
||||
AM_CONDITIONAL([HAVE_PIPEWIRE], [test "${have_pipewire}" = "yes"])
|
||||
|
||||
dnl
|
||||
dnl Pulseaudio module
|
||||
dnl
|
||||
|
@ -61,6 +61,15 @@ if HAVE_ALSA
|
||||
aout_LTLIBRARIES += libalsa_plugin.la
|
||||
endif
|
||||
|
||||
libaout_pipewire_plugin_la_SOURCES = \
|
||||
audio_output/vlc_pipewire.c audio_output/vlc_pipewire.h \
|
||||
audio_output/pipewire.c
|
||||
libaout_pipewire_plugin_la_CFLAGS = $(AM_CFLAGS) $(PIPEWIRE_CFLAGS)
|
||||
libaout_pipewire_plugin_la_LIBADD = $(PIPEWIRE_LIBS) $(LIBM)
|
||||
if HAVE_PIPEWIRE
|
||||
aout_LTLIBRARIES += libaout_pipewire_plugin.la
|
||||
endif
|
||||
|
||||
libvlc_pulse_la_SOURCES = audio_output/vlcpulse.c audio_output/vlcpulse.h
|
||||
libvlc_pulse_la_CFLAGS = $(AM_CFLAGS) $(PULSE_CFLAGS)
|
||||
libvlc_pulse_la_LIBADD = $(PULSE_LIBS) $(LTLIBVLCCORE)
|
||||
|
861
modules/audio_output/pipewire.c
Normal file
861
modules/audio_output/pipewire.c
Normal file
@ -0,0 +1,861 @@
|
||||
/*****************************************************************************
|
||||
* 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 General Public License as published by
|
||||
* the Free Software Foundation; either version 2 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 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
|
||||
|
||||
#define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
|
||||
|
||||
#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 depth;
|
||||
} queue;
|
||||
|
||||
struct {
|
||||
vlc_tick_t pts;
|
||||
ptrdiff_t frames;
|
||||
unsigned int rate;
|
||||
} time;
|
||||
|
||||
vlc_tick_t start;
|
||||
bool starting;
|
||||
bool draining;
|
||||
|
||||
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]);
|
||||
|
||||
aout_VolumeReport(s->aout, 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);
|
||||
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)
|
||||
{
|
||||
struct pw_time ts;
|
||||
|
||||
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 + s->queue.depth;
|
||||
#ifndef NDEBUG
|
||||
size_t bytes;
|
||||
vlc_frame_ChainProperties(s->queue.head, NULL, &bytes, NULL);
|
||||
assert(bytes == s->queue.depth * s->stride);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream processing callback.
|
||||
*
|
||||
* This fills in the next audio buffer.
|
||||
*/
|
||||
static void stream_process(void *data)
|
||||
{
|
||||
struct vlc_pw_stream *s = data;
|
||||
int val = stream_update_latency(s);
|
||||
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 : vlc_tick_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;
|
||||
}
|
||||
|
||||
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);
|
||||
s->queue.depth -= length / s->stride;
|
||||
|
||||
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->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 int vlc_pw_stream_get_time(struct vlc_pw_stream *s,
|
||||
vlc_tick_t *restrict delay)
|
||||
{
|
||||
int ret = -1;
|
||||
|
||||
vlc_pw_lock(s->context);
|
||||
if (pw_stream_get_state(s->stream, NULL) == PW_STREAM_STATE_STREAMING
|
||||
&& s->time.pts != VLC_TICK_INVALID) {
|
||||
vlc_tick_t elapsed = vlc_tick_now() - s->time.pts;
|
||||
*delay = vlc_tick_from_samples(s->time.frames, s->time.rate) - elapsed;
|
||||
ret = 0;
|
||||
}
|
||||
vlc_pw_unlock(s->context);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
size_t frames = block->i_buffer / s->stride;
|
||||
|
||||
assert((block->i_buffer % s->stride) == 0);
|
||||
vlc_pw_lock(s->context);
|
||||
if (pw_stream_get_state(s->stream, NULL) == 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->start = date;
|
||||
|
||||
}
|
||||
|
||||
*(s->queue.tailp) = block;
|
||||
s->queue.tailp = &block->p_next;
|
||||
s->queue.depth += frames;
|
||||
s->time.frames += frames;
|
||||
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.depth = 0;
|
||||
s->time.pts = VLC_TICK_INVALID;
|
||||
s->start = VLC_TICK_INVALID;
|
||||
s->starting = false;
|
||||
s->draining = 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);
|
||||
s->start = VLC_TICK_INVALID;
|
||||
if (pw_stream_get_state(s->stream, NULL) == PW_STREAM_STATE_ERROR)
|
||||
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;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/* 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:
|
||||
case VLC_CODEC_EAC3:
|
||||
case VLC_CODEC_DTS:
|
||||
/* TODO: pass-through */
|
||||
errno = ENOTSUP;
|
||||
return NULL;
|
||||
default:
|
||||
vlc_pw_error(sys->context, "unknown format");
|
||||
errno = ENOTSUP;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Map audio channels */
|
||||
size_t mapped = 0;
|
||||
|
||||
if (fmt->i_channels > SPA_AUDIO_MAX_CHANNELS) {
|
||||
vlc_pw_error(sys->context, "too many channels");
|
||||
errno = ENOTSUP;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const unsigned char map[] = {
|
||||
SPA_AUDIO_CHANNEL_FC,
|
||||
SPA_AUDIO_CHANNEL_FL,
|
||||
SPA_AUDIO_CHANNEL_FR,
|
||||
0 /* unassigned */,
|
||||
SPA_AUDIO_CHANNEL_RC,
|
||||
SPA_AUDIO_CHANNEL_RL,
|
||||
SPA_AUDIO_CHANNEL_RR,
|
||||
0 /* unassigned */,
|
||||
SPA_AUDIO_CHANNEL_SL,
|
||||
SPA_AUDIO_CHANNEL_SR,
|
||||
0 /* unassigned */,
|
||||
0 /* unassigned */,
|
||||
SPA_AUDIO_CHANNEL_LFE,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(map); i++)
|
||||
if ((fmt->i_physical_channels >> i) & 1)
|
||||
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));
|
||||
|
||||
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat,
|
||||
&rawfmt);
|
||||
|
||||
/* Assemble stream properties */
|
||||
struct pw_properties *props;
|
||||
|
||||
props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
|
||||
PW_KEY_MEDIA_CATEGORY, "Playback",
|
||||
NULL);
|
||||
|
||||
if (likely(props != 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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Create the stream */
|
||||
struct vlc_pw_stream *s = malloc(sizeof (*s));
|
||||
if (unlikely(s == NULL))
|
||||
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.depth = 0;
|
||||
s->time.pts = VLC_TICK_INVALID;
|
||||
s->time.rate = fmt->i_rate;
|
||||
s->start = VLC_TICK_INVALID;
|
||||
s->starting = false;
|
||||
s->draining = 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));
|
||||
|
||||
/* Wait for the stream to be ready */
|
||||
while ((state = pw_stream_get_state(s->stream,
|
||||
NULL)) == PW_STREAM_STATE_CONNECTING)
|
||||
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 int TimeGet(audio_output_t *aout, vlc_tick_t *restrict delay)
|
||||
{
|
||||
struct vlc_pw_aout *sys = aout->sys;
|
||||
return vlc_pw_stream_get_time(sys->stream, delay);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
aout->sys = sys;
|
||||
aout->start = Start;
|
||||
aout->stop = Stop;
|
||||
aout->time_get = TimeGet;
|
||||
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()
|
Loading…
Reference in New Issue
Block a user