[vlc-commits] [Git][videolan/vlc][master] 5 commits: aout: modify Makefile.am to build libvlc_pipewire.la

Steve Lhomme (@robUx4) gitlab at videolan.org
Mon Nov 25 12:08:35 UTC 2024



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
5a707fca by Ayush Dey at 2024-11-25T11:54:37+00:00
aout: modify Makefile.am to build libvlc_pipewire.la

libvlc_pipewire.la will be used to build libpipewirelist_plugin.la and libpipewiresrc_plugin.la.
This follows the same approach as using libvlc_pulse.la to build libpulselist_plugin.la.

- - - - -
663f0f09 by Ayush Dey at 2024-11-25T11:54:37+00:00
pipewire: export common helper code

- - - - -
6d491372 by Ayush Dey at 2024-11-25T11:54:37+00:00
pipewire: initial services discovery module

- - - - -
78563bdb by Ayush Dey at 2024-11-25T11:54:37+00:00
pulse: fail if the server is Pipewire

Similar to the way it has been done in d74892967268ba7bb75c62fe4f638e1437e2fc57

- - - - -
445daaa5 by Ayush Dey at 2024-11-25T11:54:37+00:00
pipewire: initial access module

- - - - -


8 changed files:

- modules/access/Makefile.am
- + modules/access/pipewire.c
- modules/audio_output/Makefile.am
- modules/audio_output/vlc_pipewire.c
- modules/audio_output/vlc_pipewire.h
- modules/services_discovery/Makefile.am
- + modules/services_discovery/pipewire.c
- modules/services_discovery/pulse.c


Changes:

=====================================
modules/access/Makefile.am
=====================================
@@ -69,6 +69,13 @@ if HAVE_JACK
 access_LTLIBRARIES += libaccess_jack_plugin.la
 endif
 
+libpipewiresrc_plugin_la_SOURCES = access/pipewire.c
+libpipewiresrc_plugin_la_CFLAGS = $(AM_CFLAGS) $(PIPEWIRE_CFLAGS)
+libpipewiresrc_plugin_la_LIBADD = libvlc_pipewire.la $(PIPEWIRE_LIBS)
+if HAVE_PIPEWIRE
+access_LTLIBRARIES += libpipewiresrc_plugin.la
+endif
+
 libpulsesrc_plugin_la_SOURCES = access/pulse.c
 libpulsesrc_plugin_la_CFLAGS= $(AM_CFLAGS) $(PULSE_CFLAGS)
 libpulsesrc_plugin_la_LIBADD = libvlc_pulse.la $(PULSE_LIBS)


=====================================
modules/access/pipewire.c
=====================================
@@ -0,0 +1,644 @@
+/*****************************************************************************
+ * @file pipewire.c
+ * @brief PipeWire input plugin for vlc
+ *****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * Authors: Ayush Dey <deyayush6 at gmail.com>
+ *          Thomas Guillem <tguillem at videolan.org>
+ * 
+ * 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 <vlc_common.h>
+#include <vlc_aout.h>
+#include <vlc_demux.h>
+#include <vlc_plugin.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/param/video/format-utils.h>
+#include <pipewire/pipewire.h>
+#include "audio_output/vlc_pipewire.h"
+
+#define HELP_TEXT N_( \
+    "Pass pipewire:// to open the default PipeWire source, " \
+    "or pipewire://SOURCE to open a specific source named SOURCE.")
+
+struct vlc_pw_stream
+{
+    struct vlc_pw_context *context;
+    struct pw_stream *stream;
+    struct spa_hook listener;
+    demux_t *demux;
+    struct spa_audio_info audio_format; /**< Audio information about the stream */
+    struct spa_video_info video_format; /**< Video information about the stream */
+    bool es_out_added; /**< Whether es_format_t structure created */
+};
+
+struct demux_sys_t
+{
+    struct vlc_pw_context *context;
+    struct vlc_pw_stream *stream;
+    struct spa_hook listener;
+    es_out_id_t *es;
+    unsigned framesize; /**< Byte size of a sample */
+    vlc_tick_t caching; /**< Caching value */
+    vlc_tick_t interval;
+    bool discontinuity; /**< The next frame will not follow the last one */
+    enum es_format_category_e media_type;
+    uint8_t chans_table[AOUT_CHAN_MAX]; /**< Channels order table */
+    uint8_t chans_to_reorder; /**< Number of channels to reorder */
+    vlc_fourcc_t format; /**< Sample format */
+};
+
+/** PipeWire audio sample (PCM) format to VLC codec */
+static vlc_fourcc_t spa_audio_format_to_fourcc(enum spa_audio_format spa_format)
+{
+    switch (spa_format)
+    {
+        case SPA_AUDIO_FORMAT_U8:
+            return VLC_CODEC_U8;
+        case SPA_AUDIO_FORMAT_ALAW:
+            return VLC_CODEC_ALAW;
+        case SPA_AUDIO_FORMAT_ULAW:
+            return VLC_CODEC_MULAW;
+        case SPA_AUDIO_FORMAT_S16_LE:
+            return VLC_CODEC_S16L;
+        case SPA_AUDIO_FORMAT_S16_BE:
+            return VLC_CODEC_S16B;
+        case SPA_AUDIO_FORMAT_F32_LE:
+            return VLC_CODEC_F32L;
+        case SPA_AUDIO_FORMAT_F32_BE:
+            return VLC_CODEC_F32B;
+        case SPA_AUDIO_FORMAT_S32_LE:
+            return VLC_CODEC_S32L;
+        case SPA_AUDIO_FORMAT_S32_BE:
+            return VLC_CODEC_S32B;
+        case SPA_AUDIO_FORMAT_S24_LE:
+            return VLC_CODEC_S24L;
+        case SPA_AUDIO_FORMAT_S24_BE:
+            return VLC_CODEC_S24B;
+        case SPA_AUDIO_FORMAT_S24_32_LE:
+            return VLC_CODEC_S24L32;
+        case SPA_AUDIO_FORMAT_S24_32_BE:
+            return VLC_CODEC_S24B32;
+        default:
+            return 0;
+    }
+}
+
+/** PipeWire video sample (PCM) format to VLC codec */
+static vlc_fourcc_t spa_video_format_to_fourcc(enum spa_video_format spa_format)
+{
+    switch (spa_format)
+    {
+        case SPA_VIDEO_FORMAT_YUY2:
+            return VLC_CODEC_YUYV;
+        case SPA_VIDEO_FORMAT_RGB:
+            return VLC_CODEC_RGB24;
+        case SPA_VIDEO_FORMAT_RGBA:
+            return VLC_CODEC_RGBA;
+        case SPA_VIDEO_FORMAT_RGBx:
+            return VLC_CODEC_RGBX;
+        case SPA_VIDEO_FORMAT_BGRx:
+            return VLC_CODEC_BGRX;
+        default:
+            return 0;
+    }
+}
+
+/** PipeWire to VLC channel mapping */
+static const uint16_t vlc_chans[] = {
+    [SPA_AUDIO_CHANNEL_MONO] = AOUT_CHAN_CENTER,
+    [SPA_AUDIO_CHANNEL_FL]   = AOUT_CHAN_LEFT,
+    [SPA_AUDIO_CHANNEL_FR]   = AOUT_CHAN_RIGHT,
+    [SPA_AUDIO_CHANNEL_RL]   = AOUT_CHAN_REARLEFT,
+    [SPA_AUDIO_CHANNEL_RR]   = AOUT_CHAN_REARRIGHT,
+    [SPA_AUDIO_CHANNEL_FC]   = AOUT_CHAN_CENTER,
+    [SPA_AUDIO_CHANNEL_LFE]  = AOUT_CHAN_LFE,
+    [SPA_AUDIO_CHANNEL_SL]   = AOUT_CHAN_MIDDLELEFT,
+    [SPA_AUDIO_CHANNEL_SR]   = AOUT_CHAN_MIDDLERIGHT,
+    [SPA_AUDIO_CHANNEL_RC]   = AOUT_CHAN_REARCENTER,
+};
+
+/**
+ * Initializes the `es_format_t fmt` object (audio properties)
+ */
+static int initialize_audio_format(struct vlc_pw_stream *s, es_format_t *fmt, vlc_fourcc_t format)
+{
+    demux_t *demux = s->demux;
+    struct demux_sys_t *sys = demux->p_sys;
+    es_format_Init(fmt, AUDIO_ES, format);
+    uint32_t channels = s->audio_format.info.raw.channels;
+
+    if (channels == 0)
+    {
+        vlc_pw_error(sys->context, "source should have at least one channel");
+        return -1;
+    }
+
+    uint32_t chans_in[AOUT_CHAN_MAX];
+    for (uint32_t i = 0; i < channels; i++)
+    {
+        uint16_t vlc_chan = 0;
+        uint32_t pos = s->audio_format.info.raw.position[i];
+        if (pos < sizeof (vlc_chans) / sizeof (vlc_chans[0]))
+            vlc_chan = vlc_chans[pos];
+
+        if (vlc_chan == 0)
+        {
+            vlc_pw_error(sys->context, "%s channel %u position %u", "unsupported", i, pos);
+            return -1;
+        }
+        chans_in[i] = vlc_chan;
+        fmt->audio.i_physical_channels |= vlc_chan;
+    }
+
+    sys->chans_to_reorder = aout_CheckChannelReorder(chans_in, NULL, fmt->audio.i_physical_channels,
+                                                     sys->chans_table);
+    sys->format = format;
+    fmt->audio.i_format = format;
+    aout_FormatPrepare(&fmt->audio);
+    fmt->audio.i_rate = s->audio_format.info.raw.rate;
+    fmt->audio.i_blockalign = fmt->audio.i_bitspersample * fmt->audio.i_channels / 8;
+    fmt->i_bitrate = fmt->audio.i_bitspersample * fmt->audio.i_channels * fmt->audio.i_rate;
+    sys->framesize = fmt->audio.i_blockalign;
+    return 0;
+}
+
+/**
+ * Initializes the `es_format_t fmt` object (video properties)
+ */
+static void initialize_video_format(struct vlc_pw_stream *s, es_format_t *fmt, vlc_fourcc_t format)
+{
+    demux_t *demux = s->demux;
+    struct demux_sys_t *sys = demux->p_sys;
+    es_format_Init(fmt, VIDEO_ES, format);
+    fmt->video.i_frame_rate = s->video_format.info.raw.framerate.num;
+    fmt->video.i_frame_rate_base = s->video_format.info.raw.framerate.denom;
+    video_format_Setup(&fmt->video, format, s->video_format.info.raw.size.width,
+                        s->video_format.info.raw.size.height, s->video_format.info.raw.size.width,
+                        s->video_format.info.raw.size.height,
+                        s->video_format.info.raw.pixel_aspect_ratio.num,
+                        s->video_format.info.raw.pixel_aspect_ratio.denom);
+    sys->interval = vlc_tick_from_samples(fmt->video.i_frame_rate,
+                                            fmt->video.i_frame_rate_base);
+}
+
+/**
+ * Parameter change callback.
+ *
+ * This callback is invoked upon stream initialization and
+ * the ES is initialized before the stream starts capturing data.
+ */
+static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+    struct vlc_pw_stream *s = data;
+
+    if(s->es_out_added) /* ES already initialized and added */
+        return;
+
+    demux_t *demux = s->demux;
+    struct demux_sys_t *sys = demux->p_sys;
+    es_format_t fmt;
+    if (param == NULL || id != SPA_PARAM_Format)
+        return;
+
+    if (sys->media_type == AUDIO_ES)
+    {
+        if (spa_format_parse(param, &s->audio_format.media_type, &s->audio_format.media_subtype) < 0)
+            return;
+
+        if (s->audio_format.media_type != SPA_MEDIA_TYPE_audio ||
+            s->audio_format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+            return;
+
+        if (spa_format_audio_raw_parse(param, &s->audio_format.info.raw) < 0)
+            return;
+
+        vlc_fourcc_t format = spa_audio_format_to_fourcc(s->audio_format.info.raw.format);
+        if (format == 0)
+        {
+            vlc_pw_error(sys->context, "unsupported PipeWire sample format %u",
+                        (unsigned)s->audio_format.info.raw.format);
+            return;
+        }
+
+        if (initialize_audio_format(s, &fmt, format))
+            return;
+    }
+    else
+    {
+        if (spa_format_parse(param, &s->video_format.media_type, &s->video_format.media_subtype) < 0)
+            return;
+
+        if (s->video_format.media_type != SPA_MEDIA_TYPE_video ||
+            s->video_format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+            return;
+
+        if (spa_format_video_raw_parse(param, &s->video_format.info.raw) < 0)
+            return;
+
+        vlc_fourcc_t format = spa_video_format_to_fourcc(s->video_format.info.raw.format);
+        if (format == 0)
+        {
+            vlc_pw_error(sys->context, "unsupported PipeWire sample format %u",
+                        (unsigned)s->video_format.info.raw.format);
+            return;
+        }
+
+        initialize_video_format(s, &fmt, format);
+    }
+
+    sys->es = es_out_Add (demux->out, &fmt);
+    s->es_out_added = true;
+}
+
+/**
+ * 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 vlc_tick_t 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 VLC_TICK_INVALID;
+
+    return (VLC_TICK_FROM_NS(ts.now) + vlc_tick_from_frac(ts.delay * ts.rate.num, ts.rate.denom));
+}
+
+/**
+ * Stream processing callback.
+ *
+ * This consumes data from the server buffer.
+ */
+static void on_process(void *data)
+{
+    struct vlc_pw_stream *s = data;
+    demux_t *demux = s->demux;
+    struct demux_sys_t *sys = demux->p_sys;
+    vlc_tick_t now;
+    vlc_tick_t val = stream_update_latency(s, &now);
+    struct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);
+
+    if (unlikely(b == NULL))
+        return;
+
+    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 length = chunk->size;
+
+    /*
+     * If timing data is not available (val == VLC_TICK_INVALID), we can at least
+     * assume that the capture delay is no less than zero.
+     */
+    vlc_tick_t pts = (val != VLC_TICK_INVALID) ? val : now;
+    es_out_SetPCR(demux->out, pts);
+    if (unlikely(sys->es == NULL))
+        goto end;
+
+    vlc_frame_t *frame = vlc_frame_Alloc(length);
+    if (likely(frame != NULL))
+    {
+        memcpy(frame->p_buffer, dst + chunk->offset, length);
+        if (sys->media_type == AUDIO_ES)
+        {
+            frame->i_nb_samples = length / sys->framesize;
+            if (sys->chans_to_reorder != 0)
+                aout_ChannelReorder(frame->p_buffer, length,
+                                    sys->chans_to_reorder, sys->chans_table,
+                                    sys->format);
+        }
+        frame->i_dts = frame->i_pts = pts;
+        if (sys->discontinuity)
+        {
+            frame->i_flags |= VLC_FRAME_FLAG_DISCONTINUITY;
+            sys->discontinuity = false;
+        }
+
+        es_out_Send(demux->out, sys->es, frame);
+    }
+    else
+        sys->discontinuity = true;
+
+    end:
+        pw_stream_queue_buffer(s->stream, b);
+}
+
+/**
+ * Disconnects a stream from source and destroys it.
+ */
+static void vlc_pw_stream_destroy(struct vlc_pw_stream *s)
+{
+    vlc_pw_lock(s->context);
+    pw_stream_flush(s->stream, false);
+    pw_stream_disconnect(s->stream);
+    pw_stream_destroy(s->stream);
+    vlc_pw_unlock(s->context);
+    free(s);
+}
+
+static const struct pw_stream_events stream_events = {
+    PW_VERSION_STREAM_EVENTS,
+    .state_changed = stream_state_changed,
+    .param_changed = on_stream_param_changed,
+    .process = on_process,
+};
+
+static int Control(demux_t *demux, int query, va_list ap)
+{
+    struct demux_sys_t *sys = demux->p_sys;
+    struct vlc_pw_stream *s = sys->stream;
+    
+    switch (query)
+    {
+        case DEMUX_GET_TIME:
+        {
+            struct pw_time ts;
+            
+            if (pw_stream_get_time_n(s->stream, &ts, sizeof(ts)) < 0)
+                return VLC_EGENERIC;
+            *(va_arg(ap, vlc_tick_t *)) = VLC_TICK_FROM_NS(ts.now);
+            break;
+        }
+
+        case DEMUX_GET_PTS_DELAY:
+        {
+            vlc_tick_t *pd = va_arg(ap, vlc_tick_t *);
+
+            *pd = sys->caching;
+            /* in case of VIDEO_ES, cap at one frame, more than enough */
+            if (sys->media_type == VIDEO_ES && *pd > sys->interval)
+                *pd = sys->interval;
+            break;
+        }
+
+        case DEMUX_HAS_UNSUPPORTED_META:
+        case DEMUX_CAN_RECORD:
+        case DEMUX_CAN_PAUSE:
+        case DEMUX_CAN_CONTROL_PACE:
+        case DEMUX_CAN_CONTROL_RATE:
+        case DEMUX_CAN_SEEK:
+            *(va_arg(ap, bool *)) = false;
+            break;
+
+        default:
+            return VLC_EGENERIC;
+    }
+
+    return VLC_SUCCESS;
+}
+
+/**
+ * Global callback
+ * This callback is used to find whether the media_type is AUDIO_ES or VIDEO_ES.
+ */
+static void registry_global(void *data, uint32_t id, uint32_t perms,
+                            const char *type, uint32_t version,
+                            const struct spa_dict *props)
+{
+    demux_t *demux = data;
+    struct demux_sys_t *sys = demux->p_sys;
+
+    if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0)
+    {
+        const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+        if (unlikely(name == NULL))
+            return;
+
+        const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+        if (class == NULL)
+            return;
+    
+        if (strstr(class, "Source") == NULL)
+            return;
+
+        if (strcmp(name, demux->psz_location) == 0)
+        {
+            if (strstr(class, "Video") != NULL)
+                sys->media_type = VIDEO_ES;
+        }
+    }
+}
+
+static const struct pw_registry_events global_events = {
+    PW_VERSION_REGISTRY_EVENTS,
+    .global = registry_global
+};
+
+static int Open(vlc_object_t *obj)
+{
+    demux_t *demux = (demux_t *)obj;
+
+    if (demux->out == NULL)
+        return VLC_EGENERIC;
+
+    struct demux_sys_t *sys = malloc(sizeof (*sys));
+    if (unlikely(sys == NULL))
+        return VLC_ENOMEM;
+
+    sys->context = vlc_pw_connect(obj, "access");
+    if (sys->context == NULL)
+    {
+        free(sys);
+        return VLC_EGENERIC;
+    }
+
+    sys->stream = NULL;
+    sys->es = NULL;
+    sys->discontinuity = false;
+    sys->caching = VLC_TICK_FROM_MS( var_InheritInteger(obj, "live-caching") );
+    sys->listener = (struct spa_hook){ };
+    sys->media_type = AUDIO_ES;
+    demux->p_sys = sys;
+
+    /* The `sys->media_type` is set to either `AUDIO_ES` or `VIDEO_ES` during this call,
+       depending on the media type of the capture node. */
+    vlc_pw_lock(sys->context);
+    vlc_pw_registry_listen(sys->context, &sys->listener, &global_events, demux);
+    vlc_pw_roundtrip_unlocked(sys->context);
+    vlc_pw_unlock(sys->context);
+
+    /* Stream parameters */
+    struct spa_audio_info_raw rawaudiofmt = {
+        .rate = 48000,
+        .channels = 2,
+        .format = SPA_AUDIO_FORMAT_S16,
+        .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }
+    };
+
+    /* Assemble the stream format and properties */
+    const struct spa_pod *params[1];
+    unsigned char buf[1024];
+    struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof (buf));
+    struct pw_properties *props;
+    const char *stream_name;
+
+    if (sys->media_type == AUDIO_ES)
+    {
+        params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat,
+                                                &rawaudiofmt);
+
+        props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+                                PW_KEY_MEDIA_CATEGORY, "Capture",
+                                PW_KEY_MEDIA_ROLE, "Raw",
+                                NULL);
+        stream_name = "audio stream";
+    }
+    else
+    {
+        params[0] = spa_pod_builder_add_object(&builder,
+                SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+                SPA_FORMAT_mediaType,       SPA_POD_Id(SPA_MEDIA_TYPE_video),
+                SPA_FORMAT_mediaSubtype,    SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+                SPA_FORMAT_VIDEO_format,    SPA_POD_CHOICE_ENUM_Id(6,
+                                                SPA_VIDEO_FORMAT_RGB,
+                                                SPA_VIDEO_FORMAT_RGB,
+                                                SPA_VIDEO_FORMAT_RGBA,
+                                                SPA_VIDEO_FORMAT_RGBx,
+                                                SPA_VIDEO_FORMAT_BGRx,
+                                                SPA_VIDEO_FORMAT_YUY2),
+                SPA_FORMAT_VIDEO_size,      SPA_POD_CHOICE_RANGE_Rectangle(
+                                                &SPA_RECTANGLE(320, 240),
+                                                &SPA_RECTANGLE(1, 1),
+                                                &SPA_RECTANGLE(4096, 4096)),
+                SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+                                                &SPA_FRACTION(25, 1),
+                                                &SPA_FRACTION(0, 1),
+                                                &SPA_FRACTION(1000, 1)));
+
+        props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+                                PW_KEY_MEDIA_CATEGORY, "Capture",
+                                PW_KEY_MEDIA_ROLE, "Camera",
+                                NULL);
+        stream_name = "video stream";
+    }
+
+    pw_properties_set(props, PW_KEY_TARGET_OBJECT, demux->psz_location);
+
+    /* Create the stream */
+    struct vlc_pw_stream *s = malloc(sizeof (*s));
+    if (unlikely(s == NULL))
+    {
+        pw_properties_free(props);
+        vlc_pw_disconnect(sys->context);
+        free(sys);
+        return VLC_EGENERIC;
+    }
+
+    enum pw_stream_flags flags =
+        PW_STREAM_FLAG_AUTOCONNECT |
+        PW_STREAM_FLAG_MAP_BUFFERS;
+
+    enum pw_stream_state state;
+
+    s->context = sys->context;
+    s->listener = (struct spa_hook){ };
+    s->es_out_added = false;
+    s->demux = demux;
+
+    vlc_pw_lock(s->context);
+    s->stream = vlc_pw_stream_new(s->context, stream_name, props);
+    if (unlikely(s->stream == NULL))
+    {
+        vlc_pw_unlock(s->context);
+        free(s);
+        pw_properties_free(props);
+        vlc_pw_disconnect(sys->context);
+        free(sys);
+        return VLC_EGENERIC;
+    }
+
+    sys->stream = s;
+    pw_stream_add_listener(s->stream, &s->listener, &stream_events, s);
+    pw_stream_connect(s->stream, PW_DIRECTION_INPUT, 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);
+            vlc_pw_disconnect(sys->context);
+            free(sys);
+            return VLC_EGENERIC;
+    }
+
+    demux->pf_demux = NULL;
+    demux->pf_control = Control;
+    return VLC_SUCCESS;
+}
+
+static void Close (vlc_object_t *obj)
+{
+    demux_t *demux = (demux_t *)obj;
+    struct demux_sys_t *sys = demux->p_sys;
+    vlc_pw_stream_destroy(sys->stream);
+    vlc_pw_disconnect(sys->context);
+    free(sys);
+}
+
+vlc_module_begin ()
+    set_shortname (N_("PipeWire"))
+    set_description (N_("PipeWire input"))
+    set_capability ("access", 0)
+    set_subcategory (SUBCAT_INPUT_ACCESS)
+    set_help (HELP_TEXT)
+
+    add_shortcut ("pipewire", "pw")
+    set_callbacks (Open, Close)
+vlc_module_end ()


=====================================
modules/audio_output/Makefile.am
=====================================
@@ -61,12 +61,18 @@ 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
+libvlc_pipewire_la_SOURCES = audio_output/vlc_pipewire.c audio_output/vlc_pipewire.h
+libvlc_pipewire_la_CFLAGS = $(AM_CFLAGS) $(PIPEWIRE_CFLAGS)
+libvlc_pipewire_la_LIBADD = $(PIPEWIRE_LIBS) $(LTLIBVLCCORE)
+libvlc_pipewire_la_LDFLAGS = \
+	-no-undefined \
+	-export-symbols-regex ^vlc_pw_ \
+	-version-info 0:0:0
+libaout_pipewire_plugin_la_SOURCES = audio_output/pipewire.c
 libaout_pipewire_plugin_la_CFLAGS = $(AM_CFLAGS) $(PIPEWIRE_CFLAGS)
-libaout_pipewire_plugin_la_LIBADD = $(PIPEWIRE_LIBS) $(LIBM)
+libaout_pipewire_plugin_la_LIBADD = libvlc_pipewire.la $(PIPEWIRE_LIBS) $(LIBM)
 if HAVE_PIPEWIRE
+pkglib_LTLIBRARIES += libvlc_pipewire.la
 aout_LTLIBRARIES += libaout_pipewire_plugin.la
 endif
 


=====================================
modules/audio_output/vlc_pipewire.c
=====================================
@@ -34,6 +34,8 @@
 #include <vlc_tick.h>
 #include "vlc_pipewire.h"
 
+const char vlc_module_name[] = "vlcpipewire";
+
 struct vlc_pw_context {
     struct pw_thread_loop *loop;
     struct pw_context *context;


=====================================
modules/audio_output/vlc_pipewire.h
=====================================
@@ -27,10 +27,10 @@ struct spa_hook;
 struct pw_properties;
 struct pw_registry_events;
 
-void vlc_pw_log(struct vlc_pw_context *ctx, int prio,
+VLC_EXPORT void vlc_pw_log(struct vlc_pw_context *ctx, int prio,
                 const char *file, unsigned int line, const char *func,
                 const char *fmt, ...);
-int vlc_pw_perror(struct vlc_pw_context *ctx, const char *file,
+VLC_EXPORT int vlc_pw_perror(struct vlc_pw_context *ctx, const char *file,
                   unsigned int line, const char *func, const char *desc);
 
 #define vlc_pw_log(ctx, prio, ...) \
@@ -44,18 +44,18 @@ int vlc_pw_perror(struct vlc_pw_context *ctx, const char *file,
 #define vlc_pw_perror(ctx, desc) \
         vlc_pw_perror(ctx, __FILE__, __LINE__, __func__, desc)
 
-void vlc_pw_lock(struct vlc_pw_context *ctx);
-void vlc_pw_unlock(struct vlc_pw_context *ctx);
-void vlc_pw_signal(struct vlc_pw_context *ctx);
-void vlc_pw_wait(struct vlc_pw_context *ctx);
+VLC_EXPORT void vlc_pw_lock(struct vlc_pw_context *ctx);
+VLC_EXPORT void vlc_pw_unlock(struct vlc_pw_context *ctx);
+VLC_EXPORT void vlc_pw_signal(struct vlc_pw_context *ctx);
+VLC_EXPORT void vlc_pw_wait(struct vlc_pw_context *ctx);
 
-struct pw_stream *vlc_pw_stream_new(struct vlc_pw_context *ctx,
+VLC_EXPORT struct pw_stream *vlc_pw_stream_new(struct vlc_pw_context *ctx,
                                     const char *name, struct pw_properties *);
 
-void vlc_pw_roundtrip_unlocked(struct vlc_pw_context *ctx);
+VLC_EXPORT void vlc_pw_roundtrip_unlocked(struct vlc_pw_context *ctx);
 
-int vlc_pw_registry_listen(struct vlc_pw_context *ctx, struct spa_hook *hook,
+VLC_EXPORT int vlc_pw_registry_listen(struct vlc_pw_context *ctx, struct spa_hook *hook,
                            const struct pw_registry_events *, void *);
 
-void vlc_pw_disconnect(struct vlc_pw_context *ctx);
-struct vlc_pw_context *vlc_pw_connect(vlc_object_t *obj, const char *name);
+VLC_EXPORT void vlc_pw_disconnect(struct vlc_pw_context *ctx);
+VLC_EXPORT struct vlc_pw_context *vlc_pw_connect(vlc_object_t *obj, const char *name);


=====================================
modules/services_discovery/Makefile.am
=====================================
@@ -41,6 +41,13 @@ if HAVE_OSX
 libupnp_plugin_la_LDFLAGS += -Wl,-framework,CoreFoundation,-framework,SystemConfiguration
 endif
 
+libpipewirelist_plugin_la_SOURCES = services_discovery/pipewire.c
+libpipewirelist_plugin_la_CFLAGS = $(AM_CFLAGS) $(PIPEWIRE_CFLAGS)
+libpipewirelist_plugin_la_LIBADD = libvlc_pipewire.la $(PIPEWIRE_LIBS)
+if HAVE_PIPEWIRE
+sd_LTLIBRARIES += libpipewirelist_plugin.la
+endif
+
 libpulselist_plugin_la_SOURCES = services_discovery/pulse.c
 libpulselist_plugin_la_CFLAGS = $(AM_CFLAGS) $(PULSE_CFLAGS)
 libpulselist_plugin_la_LIBADD = libvlc_pulse.la $(PULSE_LIBS)


=====================================
modules/services_discovery/pipewire.c
=====================================
@@ -0,0 +1,305 @@
+/*****************************************************************************
+ * @file pipewire.c
+ * @brief List of PipeWire sources for VLC media player
+ *****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * Authors: Ayush Dey <deyayush6 at gmail.com>
+ *          Thomas Guillem <tguillem at videolan.org>
+ * 
+ * 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 <search.h>
+#include <assert.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_services_discovery.h>
+#include <pipewire/pipewire.h>
+#include "audio_output/vlc_pipewire.h"
+
+struct services_discovery_sys_t
+{
+    struct vlc_pw_context *context;
+    struct spa_hook listener;
+    void *nodes;
+    void *root_card;
+};
+
+struct vlc_pw_node
+{
+    uint32_t id;
+    uint64_t serial;
+    input_item_t *item;
+    services_discovery_t *sd;
+};
+
+struct card
+{
+    input_item_t *item;
+    services_discovery_t *sd;
+    char name[];
+};
+
+static void DestroySource (void *data)
+{
+    struct vlc_pw_node *node = data;
+
+    services_discovery_RemoveItem (node->sd, node->item);
+    input_item_Release (node->item);
+    free (node);
+}
+
+static void DestroyCard(void *data)
+{
+    struct card *c = data;
+
+    services_discovery_RemoveItem(c->sd, c->item);
+    input_item_Release(c->item);
+    free(c);
+}
+
+/**
+ * Compares two nodes by ID (to support binary search).
+ */
+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 cmpcard (const void *a, const void *b)
+{
+    const struct card *ca = a, *cb = b;
+    return strcmp(ca->name, cb->name);
+}
+
+static input_item_t *AddCard (services_discovery_t *sd, const struct spa_dict *props)
+{
+    struct services_discovery_sys_t *sys = sd->p_sys;
+
+    const char *card_name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+    if (unlikely(card_name == NULL))
+        card_name = N_("Generic");
+
+    struct card *c = malloc(sizeof(*c) + strlen(card_name) + 1);
+    if (unlikely(c == NULL))
+        return NULL;
+    strcpy(c->name, card_name);
+
+    void **cp = tsearch(c, &sys->root_card, cmpcard);
+    if (cp == NULL) /* Out-of-memory */
+    {
+        free(c);
+        return NULL;
+    }
+    if (*cp != c)
+    {
+        free(c);
+        c = *cp;
+        assert(c->item != NULL);
+        return c->item;
+    }
+
+    c->item = input_item_NewExt("vlc://nop", c->name,
+                                INPUT_DURATION_INDEFINITE,
+                                ITEM_TYPE_NODE, ITEM_LOCAL);
+
+    if (unlikely(c->item == NULL))
+    {
+        tdelete(c, &sys->root_card, cmpcard);
+        free(c);
+        return NULL;
+    }
+    services_discovery_AddItem(sd, c->item);
+    c->sd = sd;
+
+    return c->item;
+}
+
+static void registry_node(services_discovery_t *sd, uint32_t id, uint32_t perms,
+                          uint32_t version, const struct spa_dict *props)
+{
+    struct services_discovery_sys_t *sys = sd->p_sys;
+    uint64_t serial;
+    char *mrl;
+
+    if (props == NULL)
+        return;
+
+    const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+    if (class == NULL)
+        return;
+    
+    if (strstr(class, "Source") == NULL)
+        return; /* Not a source */
+    
+    if (!spa_atou64(spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL), &serial, 0))
+        return;
+
+    const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+    if (unlikely(name == NULL))
+        return;
+
+    if (unlikely(asprintf (&mrl, "pipewire://%s", name) == -1))
+        return;
+
+    const char *desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+    if (unlikely(desc == NULL))
+        desc = "???";
+
+    vlc_pw_debug(sys->context, "adding %s (%s)", name, desc);
+    input_item_t *item = input_item_NewCard (mrl, desc);
+    free (mrl);
+    if (unlikely(item == NULL))
+        return;
+
+    struct vlc_pw_node *node = malloc(sizeof (*node));
+    if (unlikely(node == NULL))
+    {
+        input_item_Release (item);
+        return;
+    }
+
+    node->id = id;
+    node->serial = serial;
+    node->item = item;
+
+    struct vlc_pw_node **pp = tsearch(node, &sys->nodes, node_by_id);
+    if (unlikely(pp == NULL))
+    { /* Memory allocation error in the tree */
+        free(node);
+        input_item_Release (item);
+        return;
+    }
+    if (*pp != node)
+    { /* Existing node, update it */
+        free(node);
+        node = *pp;
+        input_item_SetURI (node->item, item->psz_uri);
+        input_item_SetName (node->item, item->psz_name);
+        input_item_Release (item);
+        return;
+    }
+
+    input_item_t *card = AddCard(sd, props);
+    services_discovery_AddSubItem(sd, card, item);
+    node->sd = sd;
+    return;
+}
+
+/**
+ * Global callback.
+ *
+ * This gets called for every initial, then every new object from the PipeWire
+ * server. We can find the usable sources 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)
+{
+    services_discovery_t *sd = data;
+
+    if (strcmp(name, PW_TYPE_INTERFACE_Node) == 0)
+        registry_node(sd, id, perms, version, props);
+}
+
+/**
+ * Global removal callback.
+ *
+ * This gets called when an object disappers. We can detect when a source is unplugged here.
+ */
+static void registry_global_remove(void *data, uint32_t id)
+{
+    services_discovery_t *sd = data;
+    struct services_discovery_sys_t *sys = sd->p_sys;
+    struct vlc_pw_node key = { .id = id };
+    struct vlc_pw_node **pp = tfind(&key, &sys->nodes, node_by_id);
+    if (pp == NULL)
+        return;
+
+    struct vlc_pw_node *node = *pp;
+    tdelete(node, &sys->nodes, node_by_id);
+    DestroySource (node);
+}
+
+static const struct pw_registry_events events = {
+    PW_VERSION_REGISTRY_EVENTS,
+    .global = registry_global,
+    .global_remove =  registry_global_remove
+};
+
+static int Open (vlc_object_t *obj)
+{
+    services_discovery_t *sd = (services_discovery_t *)obj;
+    
+    struct services_discovery_sys_t *sys = malloc(sizeof (*sys));
+    if (unlikely(sys == NULL))
+        return VLC_ENOMEM;
+
+    sys->context = vlc_pw_connect(obj, "services discovery");
+    if (sys->context == NULL)
+    {
+        free(sys);
+        return VLC_EGENERIC;
+    }
+
+    sd->p_sys = sys;
+    sd->description = _("Audio and Video capture");
+    sys->nodes = NULL;
+    sys->root_card = NULL;
+    sys->listener = (struct spa_hook){ };
+
+    vlc_pw_lock(sys->context);
+    /* Subscribe for source events */
+    vlc_pw_registry_listen(sys->context, &sys->listener, &events, sd);
+    vlc_pw_roundtrip_unlocked(sys->context); /* Enumerate existing sources */
+    vlc_pw_unlock(sys->context);
+    return VLC_SUCCESS;
+}
+
+static void Close (vlc_object_t *obj)
+{
+    services_discovery_t *sd = (services_discovery_t *)obj;
+    struct services_discovery_sys_t *sys = sd->p_sys;
+
+    vlc_pw_disconnect(sys->context);
+    tdestroy(sys->nodes, DestroySource);
+    tdestroy(sys->root_card, DestroyCard);
+    free (sys);
+}
+
+VLC_SD_PROBE_HELPER("pipewire", N_("Audio and Video capture"), SD_CAT_DEVICES);
+
+vlc_module_begin ()
+    set_shortname (N_("Audio and Video capture"))
+    set_description (N_("Audio and Video capture (PipeWire)"))
+    set_subcategory (SUBCAT_PLAYLIST_SD)
+    set_capability ("services_discovery", 0)
+    set_callbacks (Open, Close)
+    add_shortcut ("pipewire", "pw")
+
+    VLC_SD_PROBE_SUBMODULE
+vlc_module_end ()


=====================================
modules/services_discovery/pulse.c
=====================================
@@ -36,6 +36,8 @@
 static int Open (vlc_object_t *);
 static void Close (vlc_object_t *);
 
+#include <vlc_modules.h>
+
 VLC_SD_PROBE_HELPER("pulse", N_("Audio capture"), SD_CAT_DEVICES);
 
 vlc_module_begin ()
@@ -55,12 +57,29 @@ typedef struct
     void                 *root_card;
     pa_context           *context;
     pa_threaded_mainloop *mainloop;
+    bool is_pipewire;
 } services_discovery_sys_t;
 
 static void SourceCallback(pa_context *, const pa_source_info *, int, void *);
 static void ContextCallback(pa_context *, pa_subscription_event_type_t,
                             uint32_t, void *);
 
+static void server_info_cb(pa_context *ctx, const pa_server_info *info,
+                           void *userdata)
+{
+    services_discovery_t *sd = userdata;
+
+    msg_Dbg(sd, "server %s version %s on %s@%s", info->server_name,
+            info->server_version, info->user_name, info->host_name);
+
+    services_discovery_sys_t *sys = sd->p_sys;
+
+    sys->is_pipewire = strcasestr(info->server_name, "pipewire") != NULL;
+    pa_threaded_mainloop_signal(sys->mainloop, 0);
+
+    (void) ctx;
+}
+
 static int Open (vlc_object_t *obj)
 {
     services_discovery_t *sd = (services_discovery_t *)obj;
@@ -83,10 +102,27 @@ static int Open (vlc_object_t *obj)
     sys->context = ctx;
     sys->root = NULL;
     sys->root_card = NULL;
+    sys->is_pipewire = false;
+
+    pa_threaded_mainloop_lock(sys->mainloop);
+    op = pa_context_get_server_info(sys->context, server_info_cb, sd);
+    if (likely(op != NULL))
+    {
+        while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
+            pa_threaded_mainloop_wait(sys->mainloop);
+        if (sys->is_pipewire && module_exists("pipewirelist"))
+        {
+            msg_Dbg(sd, "refusing to use PipeWire");
+            pa_threaded_mainloop_unlock(sys->mainloop);
+            vlc_pa_disconnect(obj, sys->context, sys->mainloop);
+            free(sys);
+            return -ENOTSUP;
+        }
+        pa_operation_unref(op);
+    }
 
     /* Subscribe for source events */
     const pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SOURCE;
-    pa_threaded_mainloop_lock (sys->mainloop);
     pa_context_set_subscribe_callback (ctx, ContextCallback, sd);
     op = pa_context_subscribe (ctx, mask, NULL, NULL);
     if (likely(op != NULL))



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/64d46c2ba05c591b381306b8c7db1ae72ae0342a...445daaa587e71441b8eaa448639d3da85e327449

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/64d46c2ba05c591b381306b8c7db1ae72ae0342a...445daaa587e71441b8eaa448639d3da85e327449
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list