/*****************************************************************************
 * 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 3 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

#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_pipewire.h"

struct vlc_pw_stream {
    struct vlc_pw_context *context;
    struct pw_stream *stream;
    struct spa_hook listener;
    size_t stride;

    struct {
        block_t *head;
        block_t **tailp;
        size_t depth;
    } queue;

    struct {
        mtime_t pts;
        ptrdiff_t frames;
        unsigned int rate;
    } time;

    mtime_t start;
    bool starting;
    bool draining;
    bool drained;

    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;

    lldiv_t d = lldiv(ts.delay * ts.rate.num, ts.rate.denom);

    s->time.pts = ts.now / (INT64_C(1000000000) / CLOCK_FREQ);
    s->time.pts += (d.quot * CLOCK_FREQ)
                   + ((d.rem * CLOCK_FREQ) / ts.rate.denom);
    s->time.frames = ts.buffered + ts.queued + s->queue.depth;
#ifndef NDEBUG
    size_t bytes;
    block_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;
        block_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.
             */
            mtime_t pts = (val == 0) ? s->time.pts : mdate();
            mtime_t gap = s->start - pts;
            mtime_t span = (frame_room * CLOCK_FREQ) / 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 = (gap * s->time.rate) / CLOCK_FREQ;
                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;
            block_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_TS_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");
    s->drained = true;
    vlc_pw_signal(s->context);
}

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,
                                  mtime_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_TS_INVALID) {
        mtime_t elapsed = mdate() - s->time.pts;
        *delay = ((s->time.frames * CLOCK_FREQ) / 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, block_t *block,
                               mtime_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) {
        block_Release(block);
        goto out;
    }

    if (s->start == VLC_TS_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,
                                    mtime_t date)
{
    vlc_pw_lock(s->context);
    pw_stream_set_active(s->stream, !paused);
    s->time.pts = VLC_TS_INVALID;
    if (unlikely(s->starting)) {
        assert(s->start != VLC_TS_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);
    block_ChainRelease(s->queue.head);
    s->queue.head = NULL;
    s->queue.tailp = &s->queue.head;
    s->queue.depth = 0;
    s->time.pts = VLC_TS_INVALID;
    s->start = VLC_TS_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_TS_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 */
    s->drained = false;
    while (!s->drained)
        vlc_pw_wait(s->context);
    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[] = {
        /* A decimal node ID or a node name are accepted */
        SPA_DICT_ITEM_INIT(PW_KEY_NODE_TARGET, name),
    };
    struct spa_dict dict = SPA_DICT_INIT(items, ARRAY_SIZE(items));

    vlc_pw_debug(s->context, "setting node target: %s", name);
    vlc_pw_lock(s->context);
    /* FIXME: not working; use metadata API instead */
    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 aout_sys_t {
    struct vlc_pw_context *context;
    struct vlc_pw_stream *stream;
    struct spa_hook listener;
    void *nodes;
    struct {
        uint32_t device;
        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 aout_sys_t *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;
    }

    if (fmt->i_physical_channels != AOUT_CHAN_CENTER) {
        static 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];
    }
    else
        rawfmt.position[0] = SPA_AUDIO_CHANNEL_MONO;

    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_TS_INVALID;
    s->time.rate = fmt->i_rate;
    s->start = VLC_TS_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;
    }

    pw_stream_add_listener(s->stream, &s->listener, &stream_events, s);
    pw_stream_connect(s->stream, PW_DIRECTION_OUTPUT, sys->initial.device,
                      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.device = PW_ID_ANY;

    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, mtime_t *restrict delay)
{
    struct aout_sys_t *sys = aout->sys;
    return vlc_pw_stream_get_time(sys->stream, delay);
}

static void Play(audio_output_t *aout, block_t *block)
{
    struct aout_sys_t *sys = aout->sys;
    vlc_pw_stream_play(sys->stream, block, block->i_pts);
}

static void Pause(audio_output_t *aout, bool paused, mtime_t date)
{
    struct aout_sys_t *sys = aout->sys;
    vlc_pw_stream_set_pause(sys->stream, paused, date);
}

static void Flush(audio_output_t *aout, bool wait)
{
    struct aout_sys_t *sys = aout->sys;

    (wait ? vlc_pw_stream_drain : vlc_pw_stream_flush)(sys->stream);
}

static void Stop(audio_output_t *aout)
{
    struct aout_sys_t *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 aout_sys_t *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 aout_sys_t *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 aout_sys_t *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;
};

static int nodecmp(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 aout_sys_t *sys = aout->sys;

    if (sys->stream != NULL) {
        vlc_pw_stream_select_device(sys->stream, name);
        return 0;
    }

    sys->initial.device = atoi(name);
    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 aout_sys_t *sys = aout->sys;
    char idstr[11];

    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 */

    const char *desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
    if (unlikely(desc == NULL))
        desc = idstr;

    struct vlc_pw_node *node = malloc(sizeof (*node));
    if (unlikely(node == NULL))
        return;

    node->id = id;

    struct vlc_pw_node **pp = tsearch(node, &sys->nodes, nodecmp);
    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(idstr, sizeof (idstr), "%"PRIu32, id);
    aout_HotplugReport(aout, idstr, 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 aout_sys_t *sys = aout->sys;
    struct vlc_pw_node key = { .id = id };
    struct vlc_pw_node **pp = tfind(&key, &sys->nodes, nodecmp);

    if (pp != NULL) { /* A known device has been removed */
        struct vlc_pw_node *node = *pp;
        char idstr[11];

        snprintf(idstr, sizeof (idstr), "%"PRIu32, id);
        aout_HotplugReport(aout, idstr, NULL);
        tdelete(node, &sys->nodes, nodecmp);
        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 aout_sys_t *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 aout_sys_t *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.device = PW_ID_ANY;
    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->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;
}

#define VLC_MODULE_LICENSE "GPLv3"
#define VLC_MODULE_COPYRIGHT "Copyright (c) 2022 Rémi Denis-Courmont"

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()
