clapper: Add playlist demuxer element

Uses "Playlistable" enhancers to parse playlist and demux first URI in it
This commit is contained in:
Rafał Dzięgiel
2025-06-27 23:26:47 +02:00
parent 0c1d291006
commit bee2e08fb1
7 changed files with 563 additions and 6 deletions

View File

@@ -826,7 +826,8 @@ _element_setup_cb (GstElement *playbin, GstElement *element, ClapperPlayer *self
factory_name = g_intern_static_string (GST_OBJECT_NAME (factory));
GST_INFO_OBJECT (self, "Element setup: %s", factory_name);
if (factory_name == g_intern_static_string ("clapperextractablesrc")) {
if (factory_name == g_intern_static_string ("clapperextractablesrc")
|| factory_name == g_intern_static_string ("clapperplaylistdemux")) {
g_object_set (element,
"enhancer-proxies", self->enhancer_proxies,
NULL);

View File

@@ -21,6 +21,7 @@
#include <glib.h>
#include <glib-object.h>
#include <gio/gio.h>
#include <gst/gst.h>
#include "../clapper-threaded-object.h"
#include "../clapper-harvest.h"
@@ -39,4 +40,7 @@ ClapperEnhancerDirector * clapper_enhancer_director_new (void);
G_GNUC_INTERNAL
ClapperHarvest * clapper_enhancer_director_extract (ClapperEnhancerDirector *director, GList *filtered_proxies, GUri *uri, GCancellable *cancellable, GError **error);
G_GNUC_INTERNAL
GListStore * clapper_enhancer_director_parse (ClapperEnhancerDirector *director, GList *filtered_proxies, GUri *uri, GstBuffer *buffer, GCancellable *cancellable, GError **error);
G_END_DECLS

View File

@@ -25,7 +25,9 @@
#include "../clapper-cache-private.h"
#include "../clapper-enhancer-proxy-private.h"
#include "../clapper-extractable-private.h"
#include "../clapper-playlistable-private.h"
#include "../clapper-harvest-private.h"
#include "../clapper-media-item.h"
#include "../clapper-utils.h"
#include "../../shared/clapper-shared-utils-private.h"
@@ -53,6 +55,7 @@ typedef struct
ClapperEnhancerDirector *director;
GList *filtered_proxies;
GUri *uri;
GstBuffer *buffer;
GCancellable *cancellable;
GError **error;
} ClapperEnhancerDirectorData;
@@ -99,7 +102,7 @@ clapper_enhancer_director_extract_in_thread (ClapperEnhancerDirectorData *data)
success = clapper_extractable_extract (extractable, data->uri,
harvest, data->cancellable, data->error);
gst_object_unref (extractable);
g_object_unref (extractable);
/* We are done with extractable, but keep harvest and try to cache it */
if (success) {
@@ -138,6 +141,101 @@ clapper_enhancer_director_extract_in_thread (ClapperEnhancerDirectorData *data)
return harvest;
}
static gpointer
clapper_enhancer_director_parse_in_thread (ClapperEnhancerDirectorData *data)
{
ClapperEnhancerDirector *self = data->director;
GstMemory *mem;
GstMapInfo info;
GBytes *bytes;
GList *el;
GListStore *playlist = NULL;
gboolean success = FALSE;
GST_DEBUG_OBJECT (self, "Parse start");
/* Cancelled during thread switching */
if (g_cancellable_is_cancelled (data->cancellable))
return NULL;
GST_DEBUG_OBJECT (self, "Enhancer proxies for buffer: %u",
g_list_length (data->filtered_proxies));
mem = gst_buffer_peek_memory (data->buffer, 0);
if (!mem || !gst_memory_map (mem, &info, GST_MAP_READ)) {
g_set_error (data->error, GST_RESOURCE_ERROR,
GST_RESOURCE_ERROR_FAILED, "Could not read playlist buffer data");
return NULL;
}
bytes = g_bytes_new_static (info.data, info.size);
for (el = data->filtered_proxies; el; el = g_list_next (el)) {
ClapperEnhancerProxy *proxy = CLAPPER_ENHANCER_PROXY_CAST (el->data);
ClapperPlaylistable *playlistable = NULL;
if (g_cancellable_is_cancelled (data->cancellable)) // Check before loading enhancer
break;
#if CLAPPER_WITH_ENHANCERS_LOADER
playlistable = CLAPPER_PLAYLISTABLE_CAST (
clapper_enhancers_loader_create_enhancer (proxy, CLAPPER_TYPE_PLAYLISTABLE));
#endif
if (playlistable) {
GstStructure *config;
if ((config = clapper_enhancer_proxy_make_current_config (proxy))) {
clapper_enhancer_proxy_apply_config_to_enhancer (proxy, config, (GObject *) playlistable);
gst_structure_free (config);
}
if (g_cancellable_is_cancelled (data->cancellable)) { // Check before parse
g_object_unref (playlistable);
break;
}
playlist = g_list_store_new (CLAPPER_TYPE_MEDIA_ITEM); // fresh list store for each iteration
success = clapper_playlistable_parse (playlistable, data->uri, bytes,
playlist, data->cancellable, data->error);
g_object_unref (playlistable);
/* We are done with playlistable, but keep playlist */
if (success)
break;
/* Cleanup to try again with next enhancer */
g_clear_object (&playlist);
}
}
/* Unref bytes, then unmap their data */
g_bytes_unref (bytes);
gst_memory_unmap (mem, &info);
/* Cancelled during parsing */
if (g_cancellable_is_cancelled (data->cancellable))
success = FALSE;
if (!success) {
g_clear_object (&playlist);
/* Ensure we have some error set on failure */
if (*data->error == NULL) {
const gchar *err_msg = (g_cancellable_is_cancelled (data->cancellable))
? "Playlist parsing was cancelled"
: "Could not parse playlist";
g_set_error (data->error, GST_RESOURCE_ERROR,
GST_RESOURCE_ERROR_FAILED, "%s", err_msg);
}
}
GST_DEBUG_OBJECT (self, "Parse finish");
return playlist;
}
static inline void
_harvest_delete_if_expired (ClapperEnhancerDirector *self,
ClapperEnhancerProxy *proxy, GFile *file, const gint64 epoch_now)
@@ -331,6 +429,7 @@ clapper_enhancer_director_extract (ClapperEnhancerDirector *self,
data->director = self;
data->filtered_proxies = filtered_proxies;
data->uri = uri;
data->buffer = NULL;
data->cancellable = cancellable;
data->error = error;
@@ -348,6 +447,31 @@ clapper_enhancer_director_extract (ClapperEnhancerDirector *self,
return harvest;
}
GListStore *
clapper_enhancer_director_parse (ClapperEnhancerDirector *self,
GList *filtered_proxies, GUri *uri, GstBuffer *buffer,
GCancellable *cancellable, GError **error)
{
ClapperEnhancerDirectorData *data = g_new (ClapperEnhancerDirectorData, 1);
GMainContext *context;
GListStore *playlist;
data->director = self;
data->filtered_proxies = filtered_proxies;
data->uri = uri;
data->buffer = buffer;
data->cancellable = cancellable;
data->error = error;
context = clapper_threaded_object_get_context (CLAPPER_THREADED_OBJECT_CAST (self));
playlist = (GListStore *) clapper_shared_utils_context_invoke_sync_full (context,
(GThreadFunc) clapper_enhancer_director_parse_in_thread,
data, (GDestroyNotify) g_free);
return playlist;
}
static void
clapper_enhancer_director_thread_start (ClapperThreadedObject *threaded_object)
{

View File

@@ -0,0 +1,38 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library 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 library 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 library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <glib.h>
#include <glib-object.h>
#include <gst/gst.h>
#include "clapper-uri-base-demux-private.h"
G_BEGIN_DECLS
#define CLAPPER_TYPE_PLAYLIST_DEMUX (clapper_playlist_demux_get_type())
#define CLAPPER_PLAYLIST_DEMUX_CAST(obj) ((ClapperPlaylistDemux *)(obj))
G_GNUC_INTERNAL
G_DECLARE_FINAL_TYPE (ClapperPlaylistDemux, clapper_playlist_demux, CLAPPER, PLAYLIST_DEMUX, ClapperUriBaseDemux)
GST_TYPE_FIND_REGISTER_DECLARE (clapperplaylistdemux)
GST_ELEMENT_REGISTER_DECLARE (clapperplaylistdemux)
G_END_DECLS

View File

@@ -0,0 +1,383 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library 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 library 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 library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#include "clapper-playlist-demux-private.h"
#include "clapper-enhancer-director-private.h"
#include "../clapper-basic-functions.h"
#include "../clapper-enhancer-proxy.h"
#include "../clapper-enhancer-proxy-list.h"
#include "../clapper-media-item.h"
#include "../clapper-playlistable.h"
#define CLAPPER_PLAYLIST_MEDIA_TYPE "application/clapper-playlist"
#define DATA_CHUNK_SIZE 4096
#define GST_CAT_DEFAULT clapper_playlist_demux_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
struct _ClapperPlaylistDemux
{
ClapperUriBaseDemux parent;
GstCaps *caps;
ClapperEnhancerDirector *director;
ClapperEnhancerProxyList *enhancer_proxies;
};
enum
{
PROP_0,
PROP_ENHANCER_PROXIES,
PROP_LAST
};
static GParamSpec *param_specs[PROP_LAST] = { NULL, };
static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (CLAPPER_PLAYLIST_MEDIA_TYPE));
static GstStaticCaps clapper_playlist_caps = GST_STATIC_CAPS (CLAPPER_PLAYLIST_MEDIA_TYPE);
static void
clapper_playlist_type_find (GstTypeFind *tf, ClapperEnhancerProxy *proxy)
{
const gchar *prefix, *contains, *regex, *module_name;
if (!clapper_enhancer_proxy_get_target_creation_allowed (proxy))
return;
if ((prefix = clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Prefix"))) {
size_t len = strlen (prefix);
const gchar *data = (const gchar *) gst_type_find_peek (tf, 0, (guint) len);
if (!data || memcmp (data, prefix, len) != 0)
return;
}
contains = clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Contains");
regex = clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Regex");
if (contains || regex) {
const gchar *data;
guint data_size = DATA_CHUNK_SIZE;
if (!(data = (const gchar *) gst_type_find_peek (tf, 0, data_size))) {
guint64 data_len = gst_type_find_get_length (tf);
if (G_LIKELY (data_len < DATA_CHUNK_SIZE)) { // likely, since whole chunk read failed
data_size = (guint) data_len;
data = (const gchar *) gst_type_find_peek (tf, 0, data_size);
}
}
if (G_UNLIKELY (data == NULL)) {
GST_ERROR ("Could not read data!");
return;
}
if (contains && !g_strstr_len (data, data_size, contains))
return;
if (regex) {
GRegex *reg;
GError *error = NULL;
gboolean matched;
if (!(reg = g_regex_new (regex, 0, 0, &error))) {
GST_ERROR ("Could not compile regex, reason: %s", error->message);
g_error_free (error);
return;
}
matched = g_regex_match_full (reg, data, (gssize) data_size, 0, 0, NULL, NULL);
g_regex_unref (reg);
if (!matched)
return;
}
}
module_name = clapper_enhancer_proxy_get_module_name (proxy);
GST_INFO ("Suggesting likely type: " CLAPPER_PLAYLIST_MEDIA_TYPE
", enhancer: %s", module_name);
gst_type_find_suggest_simple (tf, GST_TYPE_FIND_LIKELY,
CLAPPER_PLAYLIST_MEDIA_TYPE, "enhancer", G_TYPE_STRING, module_name, NULL);
}
static gboolean
type_find_register (GstPlugin *plugin)
{
ClapperEnhancerProxyList *global_proxies = clapper_get_global_enhancer_proxies ();
GstCaps *reg_caps = NULL;
guint i, n_proxies = clapper_enhancer_proxy_list_get_n_proxies (global_proxies);
gboolean res = FALSE;
for (i = 0; i < n_proxies; ++i) {
ClapperEnhancerProxy *proxy = clapper_enhancer_proxy_list_peek_proxy (global_proxies, i);
if (clapper_enhancer_proxy_target_has_interface (proxy, CLAPPER_TYPE_PLAYLISTABLE)
&& (clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Prefix")
|| clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Contains")
|| clapper_enhancer_proxy_get_extra_data (proxy, "X-Data-Regex"))) {
if (!reg_caps)
reg_caps = gst_static_caps_get (&clapper_playlist_caps);
res |= gst_type_find_register (plugin, clapper_enhancer_proxy_get_module_name (proxy),
GST_RANK_MARGINAL + 1, (GstTypeFindFunction) clapper_playlist_type_find,
NULL, reg_caps, proxy, NULL);
}
}
gst_clear_caps (&reg_caps);
return res;
}
#define parent_class clapper_playlist_demux_parent_class
G_DEFINE_TYPE (ClapperPlaylistDemux, clapper_playlist_demux, CLAPPER_TYPE_URI_BASE_DEMUX);
GST_TYPE_FIND_REGISTER_DEFINE_CUSTOM (clapperplaylistdemux, type_find_register);
GST_ELEMENT_REGISTER_DEFINE (clapperplaylistdemux, "clapperplaylistdemux",
512, CLAPPER_TYPE_PLAYLIST_DEMUX);
static void
clapper_playlist_demux_handle_caps (ClapperUriBaseDemux *uri_bd, GstCaps *caps)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd);
gst_caps_replace (&self->caps, caps);
GST_DEBUG_OBJECT (self, "Set caps: %" GST_PTR_FORMAT, caps);
}
static GList *
_filter_playlistables (ClapperPlaylistDemux *self, GstCaps *caps, ClapperEnhancerProxyList *proxies)
{
GList *sublist = NULL;
GstStructure *structure;
ClapperEnhancerProxy *proxy;
if (caps && (structure = gst_caps_get_structure (self->caps, 0))) {
const gchar *module_name = gst_structure_get_string (structure, "enhancer");
if (module_name && (proxy = clapper_enhancer_proxy_list_get_proxy_by_module (proxies, module_name)))
sublist = g_list_append (sublist, proxy);
}
return sublist;
}
static inline gboolean
_handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable *cancellable)
{
ClapperMediaItem *item = g_list_model_get_item (G_LIST_MODEL (playlist), 0);
const gchar *uri;
gboolean success;
if (G_UNLIKELY (item == NULL)) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
("This playlist appears to be empty"), (NULL));
return FALSE;
}
uri = clapper_media_item_get_uri (item);
success = clapper_uri_base_demux_set_uri (CLAPPER_URI_BASE_DEMUX_CAST (self), uri, NULL);
gst_object_unref (item);
if (G_UNLIKELY (!success)) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
("Resolved item URI was rejected"), (NULL));
return FALSE;
}
if (!g_cancellable_is_cancelled (cancellable)) {
GstStructure *structure = gst_structure_new ("ClapperPlaylistParsed",
"playlist", G_TYPE_LIST_STORE, playlist, NULL);
gst_element_post_message (GST_ELEMENT_CAST (self),
gst_message_new_element (GST_OBJECT_CAST (self), structure));
}
return TRUE;
}
static gboolean
clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd,
GstBuffer *buffer, GCancellable *cancellable)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd);
ClapperEnhancerProxyList *proxies;
GList *filtered_proxies;
GstPad *sink_pad;
GstQuery *query;
GUri *uri = NULL;
GListStore *playlist;
GError *error = NULL;
gboolean handled;
sink_pad = gst_element_get_static_pad (GST_ELEMENT_CAST (self), "sink");
query = gst_query_new_uri ();
if (gst_pad_peer_query (sink_pad, query)) {
gchar *query_uri;
gst_query_parse_uri (query, &query_uri);
GST_DEBUG_OBJECT (self, "Source URI: %s", query_uri);
if (query_uri) {
uri = g_uri_parse (query_uri, G_URI_FLAGS_ENCODED, NULL);
g_free (query_uri);
}
}
gst_query_unref (query);
gst_object_unref (sink_pad);
if (G_UNLIKELY (uri == NULL)) {
GST_ERROR_OBJECT (self, "Could not query source URI");
return FALSE;
}
if (!self->director)
self->director = clapper_enhancer_director_new ();
GST_OBJECT_LOCK (self);
if (G_LIKELY (self->enhancer_proxies != NULL)) {
GST_INFO_OBJECT (self, "Using enhancer proxies: %" GST_PTR_FORMAT, self->enhancer_proxies);
proxies = gst_object_ref (self->enhancer_proxies);
} else {
/* Compat for old ClapperDiscoverer feature that does not set this property */
GST_WARNING_OBJECT (self, "Falling back to using global enhancer proxy list!");
proxies = gst_object_ref (clapper_get_global_enhancer_proxies ());
}
GST_OBJECT_UNLOCK (self);
filtered_proxies = _filter_playlistables (self, self->caps, proxies);
gst_object_unref (proxies);
playlist = clapper_enhancer_director_parse (self->director,
filtered_proxies, uri, buffer, cancellable, &error);
g_clear_list (&filtered_proxies, gst_object_unref);
g_uri_unref (uri);
if (!playlist) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
("%s", error->message), (NULL));
g_clear_error (&error);
return FALSE;
}
handled = _handle_playlist (self, playlist, cancellable);
g_object_unref (playlist);
return handled;
}
static void
clapper_playlist_demux_set_enhancer_proxies (ClapperPlaylistDemux *self,
ClapperEnhancerProxyList *enhancer_proxies)
{
GST_OBJECT_LOCK (self);
gst_object_replace ((GstObject **) &self->enhancer_proxies,
GST_OBJECT_CAST (enhancer_proxies));
GST_OBJECT_UNLOCK (self);
}
static void
clapper_playlist_demux_init (ClapperPlaylistDemux *self)
{
}
static void
clapper_playlist_demux_dispose (GObject *object)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (object);
GST_OBJECT_LOCK (self);
g_clear_object (&self->director);
GST_OBJECT_UNLOCK (self);
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
clapper_playlist_demux_finalize (GObject *object)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (object);
GST_TRACE_OBJECT (self, "Finalize");
gst_clear_caps (&self->caps);
gst_clear_object (&self->enhancer_proxies);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
clapper_playlist_demux_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (object);
switch (prop_id) {
case PROP_ENHANCER_PROXIES:
clapper_playlist_demux_set_enhancer_proxies (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
clapper_playlist_demux_class_init (ClapperPlaylistDemuxClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
GstElementClass *gstelement_class = (GstElementClass *) klass;
ClapperUriBaseDemuxClass *clapperuribd_class = (ClapperUriBaseDemuxClass *) klass;
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperplaylistdemux", 0,
"Clapper Playlist Demux");
gobject_class->set_property = clapper_playlist_demux_set_property;
gobject_class->dispose = clapper_playlist_demux_dispose;
gobject_class->finalize = clapper_playlist_demux_finalize;
clapperuribd_class->handle_caps = clapper_playlist_demux_handle_caps;
clapperuribd_class->process_buffer = clapper_playlist_demux_process_buffer;
param_specs[PROP_ENHANCER_PROXIES] = g_param_spec_object ("enhancer-proxies",
NULL, NULL, CLAPPER_TYPE_ENHANCER_PROXY_LIST,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
gst_element_class_add_static_pad_template (gstelement_class, &sink_template);
gst_element_class_set_static_metadata (gstelement_class, "Clapper Playlist Demux",
"Demuxer", "A custom demuxer for playlists",
"Rafał Dzięgiel <rafostar.github@gmail.com>");
}

View File

@@ -24,15 +24,17 @@
#include "../clapper-enhancer-proxy.h"
#include "../clapper-enhancer-proxy-list-private.h"
#include "../clapper-extractable.h"
#include "../clapper-playlistable.h"
#include "clapper-plugin-private.h"
#include "clapper-extractable-src-private.h"
#include "clapper-playlist-demux-private.h"
#include "clapper-uri-list-demux-private.h"
gboolean
clapper_gst_plugin_init (GstPlugin *plugin)
{
gboolean res = FALSE;
gboolean res = TRUE;
ClapperEnhancerProxyList *global_proxies;
gst_plugin_add_dependency_simple (plugin,
@@ -42,10 +44,14 @@ clapper_gst_plugin_init (GstPlugin *plugin)
global_proxies = clapper_get_global_enhancer_proxies ();
/* Avoid registering an URI handler without schemes */
if (clapper_enhancer_proxy_list_has_proxy_with_interface (global_proxies, CLAPPER_TYPE_EXTRACTABLE))
res |= GST_ELEMENT_REGISTER (clapperextractablesrc, plugin);
if (clapper_enhancer_proxy_list_has_proxy_with_interface (global_proxies, CLAPPER_TYPE_EXTRACTABLE)) {
res &= (GST_ELEMENT_REGISTER (clapperextractablesrc, plugin)
&& GST_ELEMENT_REGISTER (clapperurilistdemux, plugin));
}
res |= GST_ELEMENT_REGISTER (clapperurilistdemux, plugin);
/* Type find will only register if there are playlistable enhancers */
if (GST_TYPE_FIND_REGISTER (clapperplaylistdemux, plugin))
GST_ELEMENT_REGISTER (clapperplaylistdemux, plugin);
return res;
}

View File

@@ -164,6 +164,7 @@ clapper_sources = [
'gst/clapper-enhancer-director.c',
'gst/clapper-uri-base-demux.c',
'gst/clapper-uri-list-demux.c',
'gst/clapper-playlist-demux.c',
'../shared/clapper-shared-utils.c',
]
clapper_c_args = [