diff --git a/meson.build b/meson.build index 5f893f76..dc8fcc9a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('clapper', 'c', - version: '0.9.0', + version: '0.9.1', meson_version: '>= 0.64.0', license: 'LGPL-2.1-or-later AND GPL-3.0-or-later', # LGPL-2.1+ for libs and gst-plugin, GPL-3.0+ for app default_options: [ diff --git a/src/bin/clapper-app/clapper-app-application.c b/src/bin/clapper-app/clapper-app-application.c index 3d682fe9..6ecef41b 100644 --- a/src/bin/clapper-app/clapper-app-application.c +++ b/src/bin/clapper-app/clapper-app-application.c @@ -642,18 +642,6 @@ clapper_app_application_command_line (GApplication *app, GApplicationCommandLine return EXIT_SUCCESS; } -static gboolean -_is_claps_file (GFile *file) -{ - gchar *basename = g_file_get_basename (file); - gboolean is_claps; - - is_claps = (basename && g_str_has_suffix (basename, ".claps")); - g_free (basename); - - return is_claps; -} - static void add_item_from_file (GFile *file, ClapperQueue *queue) { @@ -666,51 +654,6 @@ add_item_from_file (GFile *file, ClapperQueue *queue) gst_object_unref (item); } -static void -add_items_from_claps_file (GFile *file, ClapperQueue *queue) -{ - GDataInputStream *dstream = NULL; - GFileInputStream *stream; - GError *error = NULL; - gchar *line; - - if (!(stream = g_file_read (file, NULL, &error))) - goto finish; - - dstream = g_data_input_stream_new (G_INPUT_STREAM (stream)); - - while ((line = g_data_input_stream_read_line ( - dstream, NULL, NULL, &error))) { - g_strstrip (line); - - if (strlen (line) > 0) { - GFile *tmp_file = gst_uri_is_valid (line) - ? g_file_new_for_uri (line) - : g_file_new_for_path (line); - - if (_is_claps_file (tmp_file)) - add_items_from_claps_file (tmp_file, queue); - else - add_item_from_file (tmp_file, queue); - - g_object_unref (tmp_file); - } - - g_free (line); - } - -finish: - if (error) { - GST_ERROR ("Could not read \".claps\" file, reason: %s", error->message); - g_error_free (error); - } - if (stream) { - g_input_stream_close (G_INPUT_STREAM (stream), NULL, NULL); - g_object_unref (stream); - } - g_clear_object (&dstream); -} - static void add_item_with_subtitles (GFile *media_file, GFile *subs_file, ClapperQueue *queue) @@ -779,12 +722,8 @@ clapper_app_application_open (GApplication *app, if (!handled) { gint i; - for (i = 0; i < n_files; ++i) { - if (_is_claps_file (files[i])) - add_items_from_claps_file (files[i], queue); - else - add_item_from_file (files[i], queue); - } + for (i = 0; i < n_files; ++i) + add_item_from_file (files[i], queue); } add_only = (g_strcmp0 (hint, "add-only") == 0); diff --git a/src/lib/clapper/clapper-app-bus-private.h b/src/lib/clapper/clapper-app-bus-private.h index cef1cfac..6f53dee4 100644 --- a/src/lib/clapper/clapper-app-bus-private.h +++ b/src/lib/clapper/clapper-app-bus-private.h @@ -44,6 +44,8 @@ void clapper_app_bus_post_refresh_streams (ClapperAppBus *app_bus, GstObject *sr void clapper_app_bus_post_refresh_timeline (ClapperAppBus *app_bus, GstObject *src); +void clapper_app_bus_post_insert_playlist (ClapperAppBus *app_bus, GstObject *src, GstObject *playlist_item, GObject *playlist); + void clapper_app_bus_post_simple_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id); void clapper_app_bus_post_object_desc_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, GstObject *object, const gchar *desc); diff --git a/src/lib/clapper/clapper-app-bus.c b/src/lib/clapper/clapper-app-bus.c index eb55d05f..b70e4fd1 100644 --- a/src/lib/clapper/clapper-app-bus.c +++ b/src/lib/clapper/clapper-app-bus.c @@ -21,6 +21,7 @@ #include "clapper-bus-private.h" #include "clapper-app-bus-private.h" #include "clapper-player-private.h" +#include "clapper-queue-private.h" #include "clapper-media-item-private.h" #include "clapper-timeline-private.h" @@ -41,6 +42,7 @@ enum CLAPPER_APP_BUS_STRUCTURE_PROP_NOTIFY, CLAPPER_APP_BUS_STRUCTURE_REFRESH_STREAMS, CLAPPER_APP_BUS_STRUCTURE_REFRESH_TIMELINE, + CLAPPER_APP_BUS_STRUCTURE_INSERT_PLAYLIST, CLAPPER_APP_BUS_STRUCTURE_SIMPLE_SIGNAL, CLAPPER_APP_BUS_STRUCTURE_OBJECT_DESC_SIGNAL, CLAPPER_APP_BUS_STRUCTURE_DESC_WITH_DETAILS_SIGNAL, @@ -52,6 +54,7 @@ static ClapperBusQuark _structure_quarks[] = { {"prop-notify", 0}, {"refresh-streams", 0}, {"refresh-timeline", 0}, + {"insert-playlist", 0}, {"simple-signal", 0}, {"object-desc-signal", 0}, {"desc-with-details-signal", 0}, @@ -65,6 +68,7 @@ enum CLAPPER_APP_BUS_FIELD_PSPEC, CLAPPER_APP_BUS_FIELD_SIGNAL_ID, CLAPPER_APP_BUS_FIELD_OBJECT, + CLAPPER_APP_BUS_FIELD_OTHER_OBJECT, CLAPPER_APP_BUS_FIELD_DESC, CLAPPER_APP_BUS_FIELD_DETAILS, CLAPPER_APP_BUS_FIELD_ERROR, @@ -76,6 +80,7 @@ static ClapperBusQuark _field_quarks[] = { {"pspec", 0}, {"signal-id", 0}, {"object", 0}, + {"other-object", 0}, {"desc", 0}, {"details", 0}, {"error", 0}, @@ -160,6 +165,36 @@ _handle_refresh_timeline_msg (GstMessage *msg, const GstStructure *structure) clapper_timeline_refresh (timeline); } +void +clapper_app_bus_post_insert_playlist (ClapperAppBus *self, GstObject *src, + GstObject *playlist_item, GObject *playlist) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (INSERT_PLAYLIST), + _FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, playlist_item, + _FIELD_QUARK (OTHER_OBJECT), G_TYPE_OBJECT, playlist, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_insert_playlist_msg (GstMessage *msg, const GstStructure *structure) +{ + ClapperPlayer *player = CLAPPER_PLAYER_CAST (GST_MESSAGE_SRC (msg)); + ClapperQueue *queue = clapper_player_get_queue (player); + GstObject *playlist_item; + GObject *playlist; + + gst_structure_id_get (structure, + _FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, &playlist_item, + _FIELD_QUARK (OTHER_OBJECT), G_TYPE_OBJECT, &playlist, + NULL); + clapper_queue_handle_playlist (queue, + CLAPPER_MEDIA_ITEM (playlist_item), G_LIST_STORE (playlist)); + + gst_object_unref (playlist_item); + g_object_unref (playlist); +} + void clapper_app_bus_post_simple_signal (ClapperAppBus *self, GstObject *src, guint signal_id) { @@ -285,6 +320,8 @@ clapper_app_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G _handle_refresh_streams_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (REFRESH_TIMELINE)) _handle_refresh_timeline_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (INSERT_PLAYLIST)) + _handle_insert_playlist_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (SIMPLE_SIGNAL)) _handle_simple_signal_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (OBJECT_DESC_SIGNAL)) diff --git a/src/lib/clapper/clapper-cache.c b/src/lib/clapper/clapper-cache.c index 86d7589c..63cd10b3 100644 --- a/src/lib/clapper/clapper-cache.c +++ b/src/lib/clapper/clapper-cache.c @@ -18,7 +18,9 @@ #include "clapper-cache-private.h" #include "clapper-version.h" + #include "clapper-extractable.h" +#include "clapper-playlistable.h" #include "clapper-reactable.h" #define CLAPPER_CACHE_HEADER "CLAPPER" @@ -26,6 +28,7 @@ typedef enum { CLAPPER_CACHE_IFACE_EXTRACTABLE = 1, + CLAPPER_CACHE_IFACE_PLAYLISTABLE, CLAPPER_CACHE_IFACE_REACTABLE, } ClapperCacheIfaces; @@ -244,6 +247,8 @@ clapper_cache_read_iface (const gchar **data) switch (iface_id) { case CLAPPER_CACHE_IFACE_EXTRACTABLE: return CLAPPER_TYPE_EXTRACTABLE; + case CLAPPER_CACHE_IFACE_PLAYLISTABLE: + return CLAPPER_TYPE_PLAYLISTABLE; case CLAPPER_CACHE_IFACE_REACTABLE: return CLAPPER_TYPE_REACTABLE; default: @@ -436,6 +441,8 @@ clapper_cache_store_iface (GByteArray *bytes, GType iface) if (iface == CLAPPER_TYPE_EXTRACTABLE) iface_id = CLAPPER_CACHE_IFACE_EXTRACTABLE; + else if (iface == CLAPPER_TYPE_PLAYLISTABLE) + iface_id = CLAPPER_CACHE_IFACE_PLAYLISTABLE; else if (iface == CLAPPER_TYPE_REACTABLE) iface_id = CLAPPER_CACHE_IFACE_REACTABLE; else diff --git a/src/lib/clapper/clapper-enhancer-proxy.c b/src/lib/clapper/clapper-enhancer-proxy.c index a7f7c8a2..76cbe4a0 100644 --- a/src/lib/clapper/clapper-enhancer-proxy.c +++ b/src/lib/clapper/clapper-enhancer-proxy.c @@ -46,12 +46,14 @@ #include "clapper-enhancer-proxy-list.h" #include "clapper-basic-functions.h" #include "clapper-cache-private.h" -#include "clapper-extractable.h" -#include "clapper-reactable.h" #include "clapper-player-private.h" #include "clapper-utils-private.h" #include "clapper-enums.h" +#include "clapper-extractable.h" +#include "clapper-playlistable.h" +#include "clapper-reactable.h" + #include "clapper-functionalities-availability.h" #if CLAPPER_WITH_ENHANCERS_LOADER @@ -464,7 +466,7 @@ gboolean clapper_enhancer_proxy_fill_from_instance (ClapperEnhancerProxy *self, GObject *enhancer) { /* NOTE: REACTABLE must be last for "allowed" to work as expected */ - const GType enhancer_types[] = { CLAPPER_TYPE_EXTRACTABLE, CLAPPER_TYPE_REACTABLE }; + const GType enhancer_types[] = { CLAPPER_TYPE_EXTRACTABLE, CLAPPER_TYPE_PLAYLISTABLE, CLAPPER_TYPE_REACTABLE }; GType *ifaces; GParamSpec **pspecs; GParamFlags enhancer_flags; @@ -1299,7 +1301,7 @@ clapper_enhancer_proxy_class_init (ClapperEnhancerProxyClass *klass) * * This effectively means whether the given enhancer can be used. * - * By default all enhancers that work on-demand such as [iface@Clapper.Extractable] + * By default all enhancers that work on-demand ([iface@Clapper.Extractable], [iface@Clapper.Playlistable]) * are allowed while enhancers implementing [iface@Clapper.Reactable] are not. * * Value of this property from a `GLOBAL` [class@Clapper.EnhancerProxyList] will carry diff --git a/src/lib/clapper/clapper-enhancers-loader.c b/src/lib/clapper/clapper-enhancers-loader.c index 1418642a..4b608968 100644 --- a/src/lib/clapper/clapper-enhancers-loader.c +++ b/src/lib/clapper/clapper-enhancers-loader.c @@ -32,6 +32,7 @@ static HMODULE _enhancers_dll_handle = NULL; // Supported interfaces #include "clapper-extractable.h" +#include "clapper-playlistable.h" #include "clapper-reactable.h" #define GST_CAT_DEFAULT clapper_enhancers_loader_debug @@ -140,7 +141,7 @@ clapper_enhancers_loader_initialize (ClapperEnhancerProxyList *proxies) * Otherwise make an instance and fill missing data from it (slow). */ if (!(filled = clapper_enhancer_proxy_fill_from_cache (proxy))) { GObject *enhancer; - const GType main_types[] = { CLAPPER_TYPE_EXTRACTABLE, CLAPPER_TYPE_REACTABLE }; + const GType main_types[] = { CLAPPER_TYPE_EXTRACTABLE, CLAPPER_TYPE_PLAYLISTABLE, CLAPPER_TYPE_REACTABLE }; guint j; /* We cannot ask libpeas for "any" of our main interfaces, so try each one until found */ diff --git a/src/lib/clapper/clapper-harvest-private.h b/src/lib/clapper/clapper-harvest-private.h index 94477cb3..4ac376ea 100644 --- a/src/lib/clapper/clapper-harvest-private.h +++ b/src/lib/clapper/clapper-harvest-private.h @@ -29,6 +29,9 @@ G_BEGIN_DECLS G_GNUC_INTERNAL ClapperHarvest * clapper_harvest_new (void); +G_GNUC_INTERNAL +void clapper_harvest_set_enhancer_in_caps (ClapperHarvest *harvest, ClapperEnhancerProxy *proxy); + G_GNUC_INTERNAL gboolean clapper_harvest_unpack (ClapperHarvest *harvest, GstBuffer **buffer, gsize *buf_size, GstCaps **caps, GstTagList **tags, GstToc **toc, GstStructure **headers); diff --git a/src/lib/clapper/clapper-harvest.c b/src/lib/clapper/clapper-harvest.c index aec1dbd9..3af1bfde 100644 --- a/src/lib/clapper/clapper-harvest.c +++ b/src/lib/clapper/clapper-harvest.c @@ -94,6 +94,13 @@ clapper_harvest_new (void) return harvest; } +void +clapper_harvest_set_enhancer_in_caps (ClapperHarvest *self, ClapperEnhancerProxy *proxy) +{ + gst_caps_set_simple (self->caps, "enhancer", G_TYPE_STRING, + clapper_enhancer_proxy_get_module_name (proxy), NULL); +} + gboolean clapper_harvest_unpack (ClapperHarvest *self, GstBuffer **buffer, gsize *buf_size, GstCaps **caps, @@ -221,7 +228,6 @@ clapper_harvest_fill_from_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro gchar *filename; const gchar *data, *read_str; const guint8 *buf_data; - guint8 *buf_copy; gsize buf_size; gint64 epoch_cached, epoch_now = 0; gdouble exp_seconds; @@ -281,10 +287,10 @@ clapper_harvest_fill_from_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro goto finish; } - /* Read media type */ + /* Read caps */ read_str = clapper_cache_read_string (&data); if (G_UNLIKELY (read_str == NULL)) { - GST_ERROR_OBJECT (self, "Could not read media type from cache file"); + GST_ERROR_OBJECT (self, "Could not read caps from cache file"); goto finish; } @@ -296,9 +302,12 @@ clapper_harvest_fill_from_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro } /* Fill harvest */ - buf_copy = g_memdup2 (buf_data, buf_size); - if (!clapper_harvest_fill (self, read_str, buf_copy, buf_size)) + if (!(self->caps = gst_caps_from_string (read_str))) { + GST_ERROR_OBJECT (self, "Could not construct caps from cache"); goto finish; + } + self->buffer = gst_buffer_new_memdup (buf_data, buf_size); + self->buf_size = buf_size; /* Read tags */ read_str = clapper_cache_read_string (&data); @@ -332,7 +341,6 @@ clapper_harvest_export_to_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro const GstStructure *config, GUri *uri) { GByteArray *bytes; - const GstStructure *caps_structure; gchar *filename, *temp_str = NULL; gboolean data_ok = TRUE; @@ -366,12 +374,13 @@ clapper_harvest_export_to_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro clapper_cache_store_string (bytes, temp_str); // NULL when no config g_clear_pointer (&temp_str, g_free); - /* Store media type */ - caps_structure = gst_caps_get_structure (self->caps, 0); - if (G_LIKELY (caps_structure != NULL)) { - clapper_cache_store_string (bytes, gst_structure_get_name (caps_structure)); + /* Store caps */ + temp_str = gst_caps_to_string (self->caps); + if (G_LIKELY (temp_str != NULL)) { + clapper_cache_store_string (bytes, temp_str); + g_clear_pointer (&temp_str, g_free); } else { - GST_ERROR_OBJECT (self, "Cannot cache empty caps"); + GST_ERROR_OBJECT (self, "Cannot cache caps"); data_ok = FALSE; } @@ -434,11 +443,15 @@ clapper_harvest_export_to_cache (ClapperHarvest *self, ClapperEnhancerProxy *pro * * Commonly used media types are: * - * * `application/dash+xml` + * * `application/dash+xml` - DASH manifest * - * * `application/x-hls` + * * `application/x-hls` - HLS manifest * - * * `text/uri-list` + * * `text/x-uri` - direct media URI + * + * * `text/uri-list` - playlist of URIs + * + * * `application/clapper-playlist` - custom playlist format * * Returns: %TRUE when filled successfully, %FALSE if taken data was empty. * @@ -459,6 +472,7 @@ clapper_harvest_fill (ClapperHarvest *self, const gchar *media_type, gpointer da if (gst_debug_category_get_threshold (GST_CAT_DEFAULT) >= GST_LEVEL_LOG) { gboolean is_printable = (strcmp (media_type, "application/dash+xml") == 0) || (strcmp (media_type, "application/x-hls") == 0) + || (strcmp (media_type, "text/x-uri") == 0) || (strcmp (media_type, "text/uri-list") == 0); if (is_printable) { diff --git a/src/lib/clapper/clapper-media-item-private.h b/src/lib/clapper/clapper-media-item-private.h index 3ae8470b..d1666355 100644 --- a/src/lib/clapper/clapper-media-item-private.h +++ b/src/lib/clapper/clapper-media-item-private.h @@ -33,6 +33,9 @@ void clapper_media_item_update_from_tag_list (ClapperMediaItem *item, const GstT G_GNUC_INTERNAL void clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDiscovererInfo *info); +G_GNUC_INTERNAL +gboolean clapper_media_item_update_from_item (ClapperMediaItem *item, ClapperMediaItem *other_item, ClapperPlayer *player); + G_GNUC_INTERNAL gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus); diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index f508e3d1..ea89529b 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -51,6 +51,10 @@ struct _ClapperMediaItem gchar *container_format; gdouble duration; + /* Whether using title from URI */ + gboolean title_is_parsed; + + GSList *redirects; gchar *cache_uri; /* For shuffle */ @@ -198,6 +202,12 @@ clapper_media_item_get_id (ClapperMediaItem *self) return self->id; } +/* FIXME: 1.0: + * Consider change to be transfer-full and just return latest data from redirects + * list (alternatively expose redirect URI). This should make it possible to work + * with enhancers that would benefit from knowledge about URI changes + * (e.g "Recall" could read actual media instead of playlist file). + */ /** * clapper_media_item_get_uri: * @item: a #ClapperMediaItem @@ -295,11 +305,11 @@ clapper_media_item_get_title (ClapperMediaItem *self) static inline gboolean _refresh_tag_prop_unlocked (ClapperMediaItem *self, const gchar *tag, - gboolean from_user, gchar **tag_ptr) + gboolean allow_overwrite, gchar **tag_ptr) { const gchar *string; - if ((*tag_ptr && from_user) // if already set, user cannot modify it + if ((*tag_ptr && !allow_overwrite) || !gst_tag_list_peek_string_index (self->tags, tag, 0, &string) // guarantees non-empty string || (g_strcmp0 (*tag_ptr, string) == 0)) return FALSE; @@ -488,11 +498,12 @@ clapper_media_item_insert_tags_internal (ClapperMediaItem *self, const GstTagLis *flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TAGS; if ((title_changed = _refresh_tag_prop_unlocked (self, GST_TAG_TITLE, - from_user, &self->title))) { + (!from_user || self->title_is_parsed), &self->title))) { + self->title_is_parsed = FALSE; *flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; } cont_changed = _refresh_tag_prop_unlocked (self, GST_TAG_CONTAINER_FORMAT, - from_user, &self->container_format); + !from_user, &self->container_format); } GST_OBJECT_UNLOCK (self); @@ -668,6 +679,61 @@ clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDisco gst_object_unref (player); } +/* XXX: Must be set from player thread */ +static inline gboolean +clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri) +{ + /* Check if we did not already redirect into that URI (prevent endless loop) */ + if (!redirect_uri || g_slist_find_custom (self->redirects, redirect_uri, (GCompareFunc) strcmp)) + return FALSE; + + self->redirects = g_slist_prepend (self->redirects, g_strdup (redirect_uri)); + GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\"", (gchar *) self->redirects->data); + + return TRUE; +} + +gboolean +clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *other_item, + ClapperPlayer *player) +{ + gboolean title_changed = FALSE; + + if (!clapper_media_item_set_redirect_uri (self, clapper_media_item_get_uri (other_item))) + return FALSE; + + GST_OBJECT_LOCK (other_item); + + if (other_item->tags) + clapper_media_item_update_from_tag_list (self, other_item->tags, player); + + /* Since its redirect now, we have to update title to describe new file instead of + * being a playlist title. If other item had parsed title, it also means that tags + * did not contain it, thus we have to manually update it and notify. */ + if (other_item->title_is_parsed) { + GST_OBJECT_LOCK (self); + title_changed = g_set_str (&self->title, other_item->title); + self->title_is_parsed = TRUE; + GST_OBJECT_UNLOCK (self); + } + + GST_OBJECT_UNLOCK (other_item); + + if (title_changed) { + ClapperReactableItemUpdatedFlags flags = CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; + ClapperFeaturesManager *features_manager; + + clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]); + + if (player->reactables_manager) + clapper_reactables_manager_trigger_item_updated (player->reactables_manager, self, flags); + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, self); + } + + return TRUE; +} + /* XXX: Must be set from player thread or upon construction */ void clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location) @@ -682,7 +748,7 @@ clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *loca } /* XXX: Can only be read from player thread. - * Returns cache URI if available, item URI otherwise. */ + * Returns cache URI if available, otherwise redirect or item URI. */ inline const gchar * clapper_media_item_get_playback_uri (ClapperMediaItem *self) { @@ -702,6 +768,9 @@ clapper_media_item_get_playback_uri (ClapperMediaItem *self) clapper_media_item_set_cache_location (self, NULL); } + if (self->redirects) + return self->redirects->data; + return self->uri; } @@ -745,6 +814,7 @@ clapper_media_item_constructed (GObject *object) self->uri = g_strdup ("file://"); self->title = clapper_utils_title_from_uri (self->uri); + self->title_is_parsed = (self->title != NULL); G_OBJECT_CLASS (parent_class)->constructed (object); } @@ -765,6 +835,7 @@ clapper_media_item_finalize (GObject *object) gst_object_unparent (GST_OBJECT_CAST (self->timeline)); gst_object_unref (self->timeline); + g_slist_free_full (self->redirects, g_free); g_free (self->cache_uri); G_OBJECT_CLASS (parent_class)->finalize (object); @@ -892,7 +963,8 @@ clapper_media_item_class_init (ClapperMediaItemClass *klass) G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /* FIXME: 1.0: Consider rename to e.g. "(menu/display)-title" - * and also make it non-nullable (return URI as final fallback) */ + * and also make it non-nullable (return URI as final fallback). + * NOTE: It would probably need to work with redirect URI */ /** * ClapperMediaItem:title: * diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index ed022c08..4cb6a1c5 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -831,6 +831,71 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player) g_free (name); g_free (details); + } else if (gst_message_has_name (msg, "ClapperPlaylistParsed")) { + ClapperMediaItem *playlist_item = NULL; + GListStore *playlist = NULL; + const GstStructure *structure = gst_message_get_structure (msg); + guint n_items; + + /* If message contains item, use that. + * Otherwise assume pending item was parsed. */ + if (gst_structure_has_field (structure, "item")) { + gst_structure_get (structure, + "item", CLAPPER_TYPE_MEDIA_ITEM, &playlist_item, NULL); + } else { + GST_OBJECT_LOCK (player); + + /* Playlist is always parsed before playback starts */ + if (player->pending_item) + playlist_item = gst_object_ref (player->pending_item); + + GST_OBJECT_UNLOCK (player); + } + + if (G_UNLIKELY (playlist_item == NULL)) { + GST_WARNING_OBJECT (player, "Playlist parsed without media item set"); + return; + } + + GST_INFO_OBJECT (player, "Received parsed playlist of %" GST_PTR_FORMAT + "(%s)", playlist_item, clapper_media_item_get_uri (playlist_item)); + + gst_structure_get (structure, + "playlist", G_TYPE_LIST_STORE, &playlist, NULL); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (playlist)); + + if (G_LIKELY (n_items > 0)) { + ClapperMediaItem *active_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + gboolean updated; + + /* Update redirect URI (must be done from player thread) */ + updated = clapper_media_item_update_from_item (playlist_item, active_item, player); + gst_object_unref (active_item); + + if (!updated) { + GstMessage *msg; + GError *error; + + error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, + "Detected infinite redirection in playlist"); + msg = gst_message_new_error (GST_OBJECT (player), error, NULL); + + _handle_error_msg (msg, player); + + g_error_free (error); + gst_message_unref (msg); + } else if (n_items > 1) { + /* Forward to append remaining items (must be done from main thread) */ + clapper_app_bus_post_insert_playlist (player->app_bus, + GST_OBJECT_CAST (player), + GST_OBJECT_CAST (playlist_item), + G_OBJECT (playlist)); + } + } + + gst_object_unref (playlist_item); + g_object_unref (playlist); } else if (gst_message_has_name (msg, "GstCacheDownloadComplete")) { ClapperMediaItem *downloaded_item = NULL; const GstStructure *structure; diff --git a/src/lib/clapper/clapper-player.c b/src/lib/clapper/clapper-player.c index 75b382e0..d01868c5 100644 --- a/src/lib/clapper/clapper-player.c +++ b/src/lib/clapper/clapper-player.c @@ -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); diff --git a/src/lib/clapper/clapper-playlistable-private.h b/src/lib/clapper/clapper-playlistable-private.h new file mode 100644 index 00000000..1deabf83 --- /dev/null +++ b/src/lib/clapper/clapper-playlistable-private.h @@ -0,0 +1,31 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +#pragma once + +#include +#include + +#include "clapper-playlistable.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +gboolean clapper_playlistable_parse (ClapperPlaylistable *playlistable, GUri *uri, GBytes *bytes, GListStore *playlist, GCancellable *cancellable, GError **error); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-playlistable.c b/src/lib/clapper/clapper-playlistable.c new file mode 100644 index 00000000..2cb1beba --- /dev/null +++ b/src/lib/clapper/clapper-playlistable.c @@ -0,0 +1,59 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +/** + * ClapperPlaylistable: + * + * An interface for creating enhancers that parse data into individual media items. + * + * Since: 0.10 + */ + +#include + +#include "clapper-playlistable-private.h" + +G_DEFINE_INTERFACE (ClapperPlaylistable, clapper_playlistable, G_TYPE_OBJECT); + +static gboolean +clapper_playlistable_default_parse (ClapperPlaylistable *self, GUri *uri, GBytes *bytes, + GListStore *playlist, GCancellable *cancellable, GError **error) +{ + if (*error == NULL) { + g_set_error (error, GST_CORE_ERROR, + GST_CORE_ERROR_NOT_IMPLEMENTED, + "Playlistable object did not implement parse function"); + } + + return FALSE; +} + +static void +clapper_playlistable_default_init (ClapperPlaylistableInterface *iface) +{ + iface->parse = clapper_playlistable_default_parse; +} + +gboolean +clapper_playlistable_parse (ClapperPlaylistable *self, GUri *uri, GBytes *bytes, + GListStore *playlist, GCancellable *cancellable, GError **error) +{ + ClapperPlaylistableInterface *iface = CLAPPER_PLAYLISTABLE_GET_IFACE (self); + + return iface->parse (self, uri, bytes, playlist, cancellable, error); +} diff --git a/src/lib/clapper/clapper-playlistable.h b/src/lib/clapper/clapper-playlistable.h new file mode 100644 index 00000000..444888df --- /dev/null +++ b/src/lib/clapper/clapper-playlistable.h @@ -0,0 +1,71 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_PLAYLISTABLE (clapper_playlistable_get_type()) +#define CLAPPER_PLAYLISTABLE_CAST(obj) ((ClapperPlaylistable *)(obj)) + +CLAPPER_API +G_DECLARE_INTERFACE (ClapperPlaylistable, clapper_playlistable, CLAPPER, PLAYLISTABLE, GObject) + +/** + * ClapperPlaylistableInterface: + * @parent_iface: The parent interface structure. + * @parse: Parse bytes and fill playlist. + */ +struct _ClapperPlaylistableInterface +{ + GTypeInterface parent_iface; + + /** + * ClapperPlaylistableInterface::parse: + * @playlistable: a #ClapperPlaylistable + * @uri: a source #GUri + * @bytes: a #GBytes + * @playlist: a #GListStore for media items + * @cancellable: a #GCancellable object + * @error: a #GError + * + * Parse @bytes and fill @playlist with [class@Clapper.MediaItem] objects. + * + * If implementation returns %FALSE, whole @playlist content will be discarded. + * + * Returns: whether parsing was successful. + * + * Since: 0.10 + */ + gboolean (* parse) (ClapperPlaylistable *playlistable, GUri *uri, GBytes *bytes, GListStore *playlist, GCancellable *cancellable, GError **error); + + /*< private >*/ + gpointer padding[8]; +}; + +G_END_DECLS diff --git a/src/lib/clapper/clapper-queue-private.h b/src/lib/clapper/clapper-queue-private.h index 87f99452..daa000c4 100644 --- a/src/lib/clapper/clapper-queue-private.h +++ b/src/lib/clapper/clapper-queue-private.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include "clapper-queue.h" #include "clapper-media-item.h" @@ -31,6 +32,8 @@ ClapperQueue * clapper_queue_new (void); void clapper_queue_handle_played_item_changed (ClapperQueue *queue, ClapperMediaItem *played_item, ClapperAppBus *app_bus); +void clapper_queue_handle_playlist (ClapperQueue *queue, ClapperMediaItem *playlist_item, GListStore *playlist); + void clapper_queue_handle_about_to_finish (ClapperQueue *queue, ClapperPlayer *player); gboolean clapper_queue_handle_eos (ClapperQueue *queue, ClapperPlayer *player); diff --git a/src/lib/clapper/clapper-queue.c b/src/lib/clapper/clapper-queue.c index 7f1ecd64..faf3b7b4 100644 --- a/src/lib/clapper/clapper-queue.c +++ b/src/lib/clapper/clapper-queue.c @@ -337,6 +337,42 @@ _get_next_item_unlocked (ClapperQueue *self, ClapperQueueProgressionMode mode) return next_item; } +static void +_take_item_unlocked (ClapperQueue *self, ClapperMediaItem *item, gint index) +{ + guint prev_length = self->items->len; + + g_ptr_array_insert (self->items, index, item); + gst_object_set_parent (GST_OBJECT_CAST (item), GST_OBJECT_CAST (self)); + + /* In append we inserted at array length */ + if (index < 0) + index = prev_length; + + _announce_model_update (self, index, 0, 1, item); + + /* If has selection and inserting before it */ + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && (guint) index <= self->current_index) { + self->current_index++; + _announce_current_index_change (self); + } else if (prev_length == 0 && _replace_current_item_unlocked (self, item, 0)) { + /* If queue was empty, auto select first item and announce it */ + _announce_current_item_and_index_change (self); + } else if (self->current_index == prev_length - 1 + && clapper_queue_get_progression_mode (self) == CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + gboolean after_eos = (gboolean) g_atomic_int_get (&player->eos); + + /* In consecutive progression automatically select next item + * if we were after EOS of last queue item */ + if (after_eos && _replace_current_item_unlocked (self, item, index)) + _announce_current_item_and_index_change (self); + + gst_object_unref (player); + } +} + /* * For gapless we need to manually replace current item in queue when it starts * playing and emit notify about change, this function will do that if necessary @@ -366,6 +402,31 @@ clapper_queue_handle_played_item_changed (ClapperQueue *self, ClapperMediaItem * } } +/* Must be called from main thread */ +void +clapper_queue_handle_playlist (ClapperQueue *self, ClapperMediaItem *playlist_item, + GListStore *playlist) +{ + GListModel *playlist_model = G_LIST_MODEL (playlist); + guint i, index, n_items = g_list_model_get_n_items (playlist_model); + + CLAPPER_QUEUE_REC_LOCK (self); + + /* If playlist item is still in the queue, insert + * remaining items after it, otherwise append */ + if (G_LIKELY (g_ptr_array_find (self->items, playlist_item, &index))) + index++; + else + index = self->items->len; + + for (i = 1; i < n_items; ++i) { + ClapperMediaItem *item = g_list_model_get_item (playlist_model, i); + _take_item_unlocked (self, item, index++); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + void clapper_queue_handle_about_to_finish (ClapperQueue *self, ClapperPlayer *player) { @@ -481,39 +542,8 @@ clapper_queue_insert_item (ClapperQueue *self, ClapperMediaItem *item, gint inde CLAPPER_QUEUE_REC_LOCK (self); - if (!g_ptr_array_find (self->items, item, NULL)) { - guint prev_length = self->items->len; - - g_ptr_array_insert (self->items, index, gst_object_ref (item)); - gst_object_set_parent (GST_OBJECT_CAST (item), GST_OBJECT_CAST (self)); - - /* In append we inserted at array length */ - if (index < 0) - index = prev_length; - - _announce_model_update (self, index, 0, 1, item); - - /* If has selection and inserting before it */ - if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION - && (guint) index <= self->current_index) { - self->current_index++; - _announce_current_index_change (self); - } else if (prev_length == 0 && _replace_current_item_unlocked (self, item, 0)) { - /* If queue was empty, auto select first item and announce it */ - _announce_current_item_and_index_change (self); - } else if (self->current_index == prev_length - 1 - && clapper_queue_get_progression_mode (self) == CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE) { - ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); - gboolean after_eos = (gboolean) g_atomic_int_get (&player->eos); - - /* In consecutive progression automatically select next item - * if we were after EOS of last queue item */ - if (after_eos && _replace_current_item_unlocked (self, item, index)) - _announce_current_item_and_index_change (self); - - gst_object_unref (player); - } - } + if (!g_ptr_array_find (self->items, item, NULL)) + _take_item_unlocked (self, gst_object_ref (item), index); CLAPPER_QUEUE_REC_UNLOCK (self); } diff --git a/src/lib/clapper/clapper.h b/src/lib/clapper/clapper.h index 4ef91391..cf176f4b 100644 --- a/src/lib/clapper/clapper.h +++ b/src/lib/clapper/clapper.h @@ -44,6 +44,7 @@ #include #include +#include #include #include diff --git a/src/lib/clapper/features/discoverer/clapper-discoverer.c b/src/lib/clapper/features/discoverer/clapper-discoverer.c index cfd62653..c9497434 100644 --- a/src/lib/clapper/features/discoverer/clapper-discoverer.c +++ b/src/lib/clapper/features/discoverer/clapper-discoverer.c @@ -39,6 +39,7 @@ */ #include +#include #include #include "clapper-discoverer.h" @@ -128,8 +129,9 @@ _run_discovery (ClapperDiscoverer *self) ClapperMediaItem *item; ClapperQueue *queue; ClapperDiscovererDiscoveryMode discovery_mode; + GstTagList *tags; const gchar *uri; - gboolean success = FALSE; + gboolean empty_tags, success = FALSE; if (self->pending_items->len == 0) { GST_DEBUG_OBJECT (self, "No more pending items"); @@ -157,6 +159,16 @@ _run_discovery (ClapperDiscoverer *self) goto finish; } + tags = clapper_media_item_get_tags (item); + empty_tags = gst_tag_list_is_empty (tags); + gst_tag_list_unref (tags); + + if (!empty_tags) { + GST_DEBUG_OBJECT (self, "Queued %" GST_PTR_FORMAT + " already has tags, ignoring discovery", item); + goto finish; + } + uri = clapper_media_item_get_uri (item); GST_DEBUG_OBJECT (self, "Starting discovery of %" GST_PTR_FORMAT "(%s)", item, uri); diff --git a/src/lib/clapper/gst/clapper-enhancer-director-private.h b/src/lib/clapper/gst/clapper-enhancer-director-private.h index 9386d12b..8bab2695 100644 --- a/src/lib/clapper/gst/clapper-enhancer-director-private.h +++ b/src/lib/clapper/gst/clapper-enhancer-director-private.h @@ -21,6 +21,7 @@ #include #include #include +#include #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 diff --git a/src/lib/clapper/gst/clapper-enhancer-director.c b/src/lib/clapper/gst/clapper-enhancer-director.c index 455df25f..9d1e075d 100644 --- a/src/lib/clapper/gst/clapper-enhancer-director.c +++ b/src/lib/clapper/gst/clapper-enhancer-director.c @@ -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,13 +102,14 @@ 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) { - if (!g_cancellable_is_cancelled (data->cancellable)) + if (!g_cancellable_is_cancelled (data->cancellable)) { + clapper_harvest_set_enhancer_in_caps (harvest, proxy); clapper_harvest_export_to_cache (harvest, proxy, config, data->uri); - + } gst_clear_structure (&config); break; } @@ -138,6 +142,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 +430,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 +448,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) { diff --git a/src/lib/clapper/gst/clapper-uri-list-demux-private.h b/src/lib/clapper/gst/clapper-harvest-uri-demux-private.h similarity index 65% rename from src/lib/clapper/gst/clapper-uri-list-demux-private.h rename to src/lib/clapper/gst/clapper-harvest-uri-demux-private.h index 6e6f1f9f..93f1f8e6 100644 --- a/src/lib/clapper/gst/clapper-uri-list-demux-private.h +++ b/src/lib/clapper/gst/clapper-harvest-uri-demux-private.h @@ -1,5 +1,5 @@ /* Clapper Playback Library - * Copyright (C) 2024 Rafał Dzięgiel + * Copyright (C) 2025 Rafał Dzięgiel * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,16 +21,17 @@ #include #include #include -#include + +#include "clapper-uri-base-demux-private.h" G_BEGIN_DECLS -#define CLAPPER_TYPE_URI_LIST_DEMUX (clapper_uri_list_demux_get_type()) -#define CLAPPER_URI_LIST_DEMUX_CAST(obj) ((ClapperUriListDemux *)(obj)) +#define CLAPPER_TYPE_HARVEST_URI_DEMUX (clapper_harvest_uri_demux_get_type()) +#define CLAPPER_HARVEST_URI_DEMUX_CAST(obj) ((ClapperHarvestUriDemux *)(obj)) G_GNUC_INTERNAL -G_DECLARE_FINAL_TYPE (ClapperUriListDemux, clapper_uri_list_demux, CLAPPER, URI_LIST_DEMUX, GstBin) +G_DECLARE_FINAL_TYPE (ClapperHarvestUriDemux, clapper_harvest_uri_demux, CLAPPER, HARVEST_URI_DEMUX, ClapperUriBaseDemux) -GST_ELEMENT_REGISTER_DECLARE (clapperurilistdemux) +GST_ELEMENT_REGISTER_DECLARE (clapperharvesturidemux) G_END_DECLS diff --git a/src/lib/clapper/gst/clapper-harvest-uri-demux.c b/src/lib/clapper/gst/clapper-harvest-uri-demux.c new file mode 100644 index 00000000..18a8ef87 --- /dev/null +++ b/src/lib/clapper/gst/clapper-harvest-uri-demux.c @@ -0,0 +1,190 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +#include "clapper-harvest-uri-demux-private.h" + +#define GST_CAT_DEFAULT clapper_harvest_uri_demux_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperHarvestUriDemux +{ + ClapperUriBaseDemux parent; + + GMutex lock; + GstStructure *http_headers; +}; + +static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("text/x-uri, source=(string)clapper-harvest")); + +#define parent_class clapper_harvest_uri_demux_parent_class +G_DEFINE_TYPE (ClapperHarvestUriDemux, clapper_harvest_uri_demux, CLAPPER_TYPE_URI_BASE_DEMUX); +GST_ELEMENT_REGISTER_DEFINE (clapperharvesturidemux, "clapperharvesturidemux", + 512, CLAPPER_TYPE_HARVEST_URI_DEMUX); + +static void +_set_property (GstObject *obj, const gchar *prop_name, gpointer value) +{ + g_object_set (G_OBJECT (obj), prop_name, value, NULL); + + if (gst_debug_category_get_threshold (GST_CAT_DEFAULT) >= GST_LEVEL_DEBUG) { + gchar *el_name; + + el_name = gst_object_get_name (obj); + GST_DEBUG ("Set %s %s", el_name, prop_name); + + g_free (el_name); + } +} + +static gboolean +configure_deep_element (GQuark field_id, const GValue *value, GstElement *child) +{ + GObjectClass *gobject_class; + const GstStructure *substructure; + + if (!GST_VALUE_HOLDS_STRUCTURE (value)) + return TRUE; + + substructure = gst_value_get_structure (value); + + if (!gst_structure_has_name (substructure, "request-headers")) + return TRUE; + + gobject_class = G_OBJECT_GET_CLASS (child); + + if (g_object_class_find_property (gobject_class, "user-agent")) { + const gchar *ua; + + if ((ua = gst_structure_get_string (substructure, "User-Agent"))) + _set_property (GST_OBJECT_CAST (child), "user-agent", (gchar *) ua); + } + + if (g_object_class_find_property (gobject_class, "extra-headers")) { + GstStructure *extra_headers; + + extra_headers = gst_structure_copy (substructure); + gst_structure_set_name (extra_headers, "extra-headers"); + gst_structure_remove_field (extra_headers, "User-Agent"); + + _set_property (GST_OBJECT_CAST (child), "extra-headers", extra_headers); + + gst_structure_free (extra_headers); + } + + return TRUE; +} + +static void +clapper_harvest_uri_demux_deep_element_added (GstBin *bin, GstBin *sub_bin, GstElement *child) +{ + if (GST_OBJECT_FLAG_IS_SET (child, GST_ELEMENT_FLAG_SOURCE)) { + ClapperHarvestUriDemux *self = CLAPPER_HARVEST_URI_DEMUX_CAST (bin); + + g_mutex_lock (&self->lock); + + if (self->http_headers) { + gst_structure_foreach (self->http_headers, + (GstStructureForeachFunc) configure_deep_element, child); + } + + g_mutex_unlock (&self->lock); + } +} + +static gboolean +clapper_harvest_uri_demux_process_buffer (ClapperUriBaseDemux *uri_bd, + GstBuffer *buffer, GCancellable *cancellable) +{ + GstMemory *mem = gst_buffer_peek_memory (buffer, 0); + GstMapInfo info; + gboolean success = FALSE; + + if (mem && gst_memory_map (mem, &info, GST_MAP_READ)) { + success = clapper_uri_base_demux_set_uri (uri_bd, + (const gchar *) info.data, "clapperextractablesrc"); + gst_memory_unmap (mem, &info); + } + + return success; +} + +static void +clapper_harvest_uri_demux_handle_custom_event (ClapperUriBaseDemux *uri_bd, GstEvent *event) +{ + const GstStructure *structure = gst_event_get_structure (event); + + if (structure && gst_structure_has_name (structure, "http-headers")) { + ClapperHarvestUriDemux *self = CLAPPER_HARVEST_URI_DEMUX_CAST (uri_bd); + + GST_DEBUG_OBJECT (self, "Received \"http-headers\" custom event"); + + g_mutex_lock (&self->lock); + + gst_clear_structure (&self->http_headers); + self->http_headers = gst_structure_copy (structure); + + g_mutex_unlock (&self->lock); + } +} + +static void +clapper_harvest_uri_demux_init (ClapperHarvestUriDemux *self) +{ + g_mutex_init (&self->lock); +} + +static void +clapper_harvest_uri_demux_finalize (GObject *object) +{ + ClapperHarvestUriDemux *self = CLAPPER_HARVEST_URI_DEMUX_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + gst_clear_structure (&self->http_headers); + g_mutex_clear (&self->lock); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_harvest_uri_demux_class_init (ClapperHarvestUriDemuxClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + GstElementClass *gstelement_class = (GstElementClass *) klass; + GstBinClass *gstbin_class = (GstBinClass *) klass; + ClapperUriBaseDemuxClass *clapperuribd_class = (ClapperUriBaseDemuxClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperharvesturidemux", 0, + "Clapper Harvest URI Demux"); + + gobject_class->finalize = clapper_harvest_uri_demux_finalize; + + gstbin_class->deep_element_added = clapper_harvest_uri_demux_deep_element_added; + + clapperuribd_class->process_buffer = clapper_harvest_uri_demux_process_buffer; + clapperuribd_class->handle_custom_event = clapper_harvest_uri_demux_handle_custom_event; + + gst_element_class_add_static_pad_template (gstelement_class, &sink_template); + + gst_element_class_set_static_metadata (gstelement_class, "Clapper Harvest URI Demux", + "Demuxer", "A custom demuxer for harvested URI", + "Rafał Dzięgiel "); +} diff --git a/src/lib/clapper/gst/clapper-playlist-demux-private.h b/src/lib/clapper/gst/clapper-playlist-demux-private.h new file mode 100644 index 00000000..79bd17d2 --- /dev/null +++ b/src/lib/clapper/gst/clapper-playlist-demux-private.h @@ -0,0 +1,38 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +#pragma once + +#include +#include +#include + +#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 diff --git a/src/lib/clapper/gst/clapper-playlist-demux.c b/src/lib/clapper/gst/clapper-playlist-demux.c new file mode 100644 index 00000000..fb123240 --- /dev/null +++ b/src/lib/clapper/gst/clapper-playlist-demux.c @@ -0,0 +1,514 @@ +/* Clapper Playback Library + * Copyright (C) 2025 Rafał Dzięgiel + * + * 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 + * . + */ + +#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 CLAPPER_CLAPS_MEDIA_TYPE "text/clapper-claps" +#define URI_LIST_MEDIA_TYPE "text/uri-list" +#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 ";" CLAPPER_CLAPS_MEDIA_TYPE ";" URI_LIST_MEDIA_TYPE)); + +static GstStaticCaps clapper_playlist_caps = GST_STATIC_CAPS (CLAPPER_PLAYLIST_MEDIA_TYPE); +static GstStaticCaps clapper_claps_caps = GST_STATIC_CAPS (CLAPPER_CLAPS_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); +} + +/* Finds text file of full file paths. Claps file might also use URIs, + * but in that case lets GStreamer built-in type finders find that as + * "text/uri-list" and we will handle it with this element too. */ +static void +clapper_claps_type_find (GstTypeFind *tf, gpointer user_data G_GNUC_UNUSED) +{ + const guint8 *data; + + if ((data = gst_type_find_peek (tf, 0, 3))) { + gboolean possible; + + /* Linux file path */ + possible = (data[0] == '/' && g_ascii_isalnum (data[1])); + +#ifdef G_OS_WIN32 + /* Windows file path ("C:\..." or "D:/...") */ + if (!possible) + possible = (g_ascii_isalpha (data[0]) && data[1] == ':' && (data[2] == '\\' || data[2] == '/')); + + /* Windows UNC Path */ + if (!possible) + possible = (data[0] == '\\' && data[1] == '\\' && g_ascii_isalnum (data[2])); +#endif + + if (possible) { + GST_INFO ("Suggesting possible type: " CLAPPER_CLAPS_MEDIA_TYPE); + gst_type_find_suggest_empty_simple (tf, GST_TYPE_FIND_POSSIBLE, CLAPPER_CLAPS_MEDIA_TYPE); + } + } +} + +static gboolean +type_find_register (GstPlugin *plugin) +{ + ClapperEnhancerProxyList *global_proxies = clapper_get_global_enhancer_proxies (); + GstCaps *reg_caps; + guint i, n_proxies = clapper_enhancer_proxy_list_get_n_proxies (global_proxies); + gboolean res; + + reg_caps = gst_static_caps_get (&clapper_claps_caps); + res = gst_type_find_register (plugin, "clapper-claps", + GST_RANK_MARGINAL + 1, (GstTypeFindFunction) clapper_claps_type_find, + "claps", reg_caps, NULL, NULL); + gst_clear_caps (®_caps); + + 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 (®_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 GListStore * +_parse_uri_list (ClapperPlaylistDemux *self, GUri *uri, GstBuffer *buffer, + GCancellable *cancellable, GError **error) +{ + GListStore *playlist; + GstMemory *mem; + GstMapInfo info; + const gchar *ptr, *end; + + mem = gst_buffer_peek_memory (buffer, 0); + if (!mem || !gst_memory_map (mem, &info, GST_MAP_READ)) { + g_set_error (error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, + "Could not read URI list buffer data"); + return NULL; + } + + playlist = g_list_store_new (CLAPPER_TYPE_MEDIA_ITEM); + ptr = (gchar *) info.data; + end = ptr + info.size; + + while (ptr < end) { + ClapperMediaItem *item = NULL; + const gchar *nl = memchr (ptr, '\n', end - ptr); + gsize len = nl ? nl - ptr : end - ptr; + gchar *line; + + if (g_cancellable_is_cancelled (cancellable)) + break; + + line = g_strndup (ptr, len); + GST_DEBUG_OBJECT (self, "Parsing line: %s", line); + + if (gst_uri_is_valid (line)) { + GST_DEBUG_OBJECT (self, "Found URI: %s", line); + item = clapper_media_item_new (line); + } else { + gchar *base_uri, *res_uri; + + base_uri = g_uri_to_string (uri); + res_uri = g_uri_resolve_relative (base_uri, line, G_URI_FLAGS_ENCODED, error); + g_free (base_uri); + + if (res_uri) { + GST_DEBUG_OBJECT (self, "Resolved URI: %s", res_uri); + item = clapper_media_item_new (res_uri); + g_free (res_uri); + } + } + + g_free (line); + + if (G_UNLIKELY (*error != NULL)) { + g_clear_object (&playlist); + break; + } + + if (G_LIKELY (item != NULL)) + g_list_store_append (playlist, (GObject *) item); + + /* Advance to the next line */ + ptr = nl ? (nl + 1) : end; + } + + gst_memory_unmap (mem, &info); + + return playlist; +} + +static gboolean +_caps_have_media_type (GstCaps *caps, const gchar *media_type) +{ + GstStructure *structure; + gboolean is_media_type = FALSE; + + if (caps && (structure = gst_caps_get_structure (caps, 0))) + is_media_type = gst_structure_has_name (structure, media_type); + + return is_media_type; +} + +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); + 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 (_caps_have_media_type (self->caps, CLAPPER_PLAYLIST_MEDIA_TYPE)) { + ClapperEnhancerProxyList *proxies; + GList *filtered_proxies; + + 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); + + if (!self->director) + self->director = clapper_enhancer_director_new (); + + 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); + } else if (_caps_have_media_type (self->caps, URI_LIST_MEDIA_TYPE) + || _caps_have_media_type (self->caps, CLAPPER_CLAPS_MEDIA_TYPE)) { + playlist = _parse_uri_list (self, uri, buffer, cancellable, &error); + } else { // Should never happen + playlist = NULL; + error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, + "Unsupported media type in caps"); + } + + 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 "); +} diff --git a/src/lib/clapper/gst/clapper-plugin.c b/src/lib/clapper/gst/clapper-plugin.c index 43a5ae45..9c7da0c3 100644 --- a/src/lib/clapper/gst/clapper-plugin.c +++ b/src/lib/clapper/gst/clapper-plugin.c @@ -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-uri-list-demux-private.h" +#include "clapper-harvest-uri-demux-private.h" +#include "clapper-playlist-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 (clapperharvesturidemux, 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; } diff --git a/src/lib/clapper/gst/clapper-uri-base-demux-private.h b/src/lib/clapper/gst/clapper-uri-base-demux-private.h new file mode 100644 index 00000000..3237c419 --- /dev/null +++ b/src/lib/clapper/gst/clapper-uri-base-demux-private.h @@ -0,0 +1,48 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * 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 + * . + */ + +#pragma once + +#include +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_URI_BASE_DEMUX (clapper_uri_base_demux_get_type()) +#define CLAPPER_URI_BASE_DEMUX_CAST(obj) ((ClapperUriBaseDemux *)(obj)) + +G_GNUC_INTERNAL +G_DECLARE_DERIVABLE_TYPE (ClapperUriBaseDemux, clapper_uri_base_demux, CLAPPER, URI_BASE_DEMUX, GstBin) + +struct _ClapperUriBaseDemuxClass +{ + GstBinClass parent_class; + + gboolean (* process_buffer) (ClapperUriBaseDemux *uri_bd, GstBuffer *buffer, GCancellable *cancellable); + + void (* handle_caps) (ClapperUriBaseDemux *uri_bd, GstCaps *caps); + + void (* handle_custom_event) (ClapperUriBaseDemux *uri_bd, GstEvent *event); +}; + +gboolean clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *uri_bd, const gchar *uri, const gchar *blacklisted_el); + +G_END_DECLS diff --git a/src/lib/clapper/gst/clapper-uri-base-demux.c b/src/lib/clapper/gst/clapper-uri-base-demux.c new file mode 100644 index 00000000..45bcdd97 --- /dev/null +++ b/src/lib/clapper/gst/clapper-uri-base-demux.c @@ -0,0 +1,408 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * 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 + * . + */ + +#include + +#include "clapper-uri-base-demux-private.h" + +#define GST_CAT_DEFAULT clapper_uri_base_demux_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef struct _ClapperUriBaseDemuxPrivate ClapperUriBaseDemuxPrivate; + +struct _ClapperUriBaseDemuxPrivate +{ + GstAdapter *input_adapter; + + GstElement *uri_handler; + GstElement *typefind; + + GstPad *typefind_src; + + GCancellable *cancellable; +}; + +typedef struct +{ + const gchar *search_proto; + const gchar *blacklisted_el; +} ClapperUriBaseDemuxFilterData; + +static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS_ANY); + +#define parent_class clapper_uri_base_demux_parent_class +G_DEFINE_TYPE_WITH_PRIVATE (ClapperUriBaseDemux, clapper_uri_base_demux, GST_TYPE_BIN); + +static gboolean +remove_sometimes_pad_cb (GstElement *element, GstPad *pad, ClapperUriBaseDemux *self) +{ + GstPadTemplate *template = gst_pad_get_pad_template (pad); + GstPadPresence presence = GST_PAD_TEMPLATE_PRESENCE (template); + + gst_object_unref (template); + + if (presence == GST_PAD_SOMETIMES) { + GST_DEBUG_OBJECT (self, "Removing src pad"); + + gst_pad_set_active (pad, FALSE); + + if (G_UNLIKELY (!gst_element_remove_pad (element, pad))) + g_critical ("Failed to remove pad from bin"); + } + + return TRUE; +} + +static void +clapper_uri_base_demux_reset (ClapperUriBaseDemux *self) +{ + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + GstElement *element = GST_ELEMENT_CAST (self); + + GST_OBJECT_LOCK (self); + + GST_LOG_OBJECT (self, "Resetting cancellable"); + + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = g_cancellable_new (); + + GST_OBJECT_UNLOCK (self); + + gst_element_foreach_pad (element, (GstElementForeachPadFunc) remove_sometimes_pad_cb, NULL); +} + +static GstStateChangeReturn +clapper_uri_base_demux_change_state (GstElement *element, GstStateChange transition) +{ + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (element); + GstStateChangeReturn ret; + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + if (ret == GST_STATE_CHANGE_FAILURE) + return ret; + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + clapper_uri_base_demux_reset (self); + break; + default: + break; + } + + return ret; +} + +static gboolean +_feature_filter (GstPluginFeature *feature, ClapperUriBaseDemuxFilterData *filter_data) +{ + GstElementFactory *factory; + const gchar *const *protocols; + const gchar *feature_name; + guint i; + + if (!GST_IS_ELEMENT_FACTORY (feature)) + return FALSE; + + factory = GST_ELEMENT_FACTORY_CAST (feature); + + if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC) + return FALSE; + + feature_name = gst_plugin_feature_get_name (feature); + + /* Do not loop endlessly creating our own sources and demuxers */ + if (!feature_name || g_strcmp0 (feature_name, filter_data->blacklisted_el) == 0) + return FALSE; + + protocols = gst_element_factory_get_uri_protocols (factory); + + if (protocols) { + for (i = 0; protocols[i]; ++i) { + if (g_ascii_strcasecmp (protocols[i], filter_data->search_proto) == 0) + return TRUE; + } + } + + return FALSE; +} + +static GstElement * +_make_handler_for_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar *blacklisted_el) +{ + GstElement *element = NULL; + GList *factories, *f; + ClapperUriBaseDemuxFilterData filter_data; + gchar *protocol; + + if (!gst_uri_is_valid (uri)) { + GST_ERROR_OBJECT (self, "Cannot create handler for invalid URI: \"%s\"", uri); + return NULL; + } + + protocol = gst_uri_get_protocol (uri); + + filter_data.search_proto = protocol; + filter_data.blacklisted_el = blacklisted_el; + + factories = gst_registry_feature_filter (gst_registry_get (), + (GstPluginFeatureFilter) _feature_filter, FALSE, &filter_data); + + g_free (protocol); + + factories = g_list_sort (factories, + (GCompareFunc) gst_plugin_feature_rank_compare_func); + + for (f = factories; f; f = g_list_next (f)) { + GstElementFactory *factory = f->data; + + if ((element = gst_element_factory_create (factory, NULL)) + && gst_uri_handler_set_uri (GST_URI_HANDLER (element), uri, NULL)) + break; + + gst_clear_object (&element); + } + + gst_plugin_feature_list_free (factories); + + GST_DEBUG_OBJECT (self, "Created URI handler: %s", + GST_OBJECT_NAME (element)); + + return element; +} + +gboolean +clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar *blacklisted_el) +{ + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + GstPad *uri_handler_src, *typefind_sink, *src_ghostpad; + GstPadLinkReturn pad_link_ret; + + GST_DEBUG_OBJECT (self, "Stream URI: %s", uri); + + if (priv->uri_handler) { + GST_DEBUG_OBJECT (self, "Trying to reuse existing URI handler"); + + if (gst_uri_handler_set_uri (GST_URI_HANDLER (priv->uri_handler), uri, NULL)) { + GST_DEBUG_OBJECT (self, "Reused existing URI handler"); + } else { + GST_DEBUG_OBJECT (self, "Could not reuse existing URI handler"); + + if (priv->typefind_src) { + gst_element_remove_pad (GST_ELEMENT_CAST (self), priv->typefind_src); + gst_clear_object (&priv->typefind_src); + } + + gst_bin_remove (GST_BIN_CAST (self), priv->uri_handler); + gst_bin_remove (GST_BIN_CAST (self), priv->typefind); + + priv->uri_handler = NULL; + priv->typefind = NULL; + } + } + + if (!priv->uri_handler) { + GST_DEBUG_OBJECT (self, "Creating new URI handler element"); + + priv->uri_handler = _make_handler_for_uri (self, uri, blacklisted_el); + + if (G_UNLIKELY (!priv->uri_handler)) { + GST_ERROR_OBJECT (self, "Could not create URI handler element"); + + GST_ELEMENT_ERROR (self, CORE, MISSING_PLUGIN, + ("Missing plugin to handle URI: %s", uri), (NULL)); + + return FALSE; + } + + gst_bin_add (GST_BIN_CAST (self), priv->uri_handler); + + priv->typefind = gst_element_factory_make ("typefind", NULL); + gst_bin_add (GST_BIN_CAST (self), priv->typefind); + + uri_handler_src = gst_element_get_static_pad (priv->uri_handler, "src"); + typefind_sink = gst_element_get_static_pad (priv->typefind, "sink"); + + pad_link_ret = gst_pad_link_full (uri_handler_src, typefind_sink, + GST_PAD_LINK_CHECK_NOTHING); + + if (pad_link_ret != GST_PAD_LINK_OK) + g_critical ("Failed to link bin elements"); + + g_object_unref (uri_handler_src); + g_object_unref (typefind_sink); + + priv->typefind_src = gst_element_get_static_pad (priv->typefind, "src"); + + src_ghostpad = gst_ghost_pad_new_from_template ("src", priv->typefind_src, + gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (self), "src")); + + gst_pad_set_active (src_ghostpad, TRUE); + + if (!gst_element_add_pad (GST_ELEMENT_CAST (self), src_ghostpad)) { + g_critical ("Failed to add source pad to bin"); + } else { + GST_DEBUG_OBJECT (self, "Added src pad, signalling \"no-more-pads\""); + gst_element_no_more_pads (GST_ELEMENT_CAST (self)); + } + } + + gst_element_sync_state_with_parent (priv->typefind); + gst_element_sync_state_with_parent (priv->uri_handler); + + return TRUE; +} + +static gboolean +clapper_uri_base_demux_sink_event (GstPad *pad, GstObject *parent, GstEvent *event) +{ + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (parent); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_CAPS:{ + ClapperUriBaseDemuxClass *uri_bd_class = CLAPPER_URI_BASE_DEMUX_GET_CLASS (self); + + if (uri_bd_class->handle_caps) { + GstCaps *caps; + + gst_event_parse_caps (event, &caps); + + if (gst_caps_is_fixed (caps)) + uri_bd_class->handle_caps (self, caps); + } + break; + } + case GST_EVENT_EOS:{ + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + GCancellable *cancellable; + GstBuffer *buffer; + gsize size; + gboolean success; + + size = gst_adapter_available (priv->input_adapter); + + if (size == 0) { + GST_WARNING_OBJECT (self, "Received EOS without URI data"); + break; + } + + GST_OBJECT_LOCK (self); + cancellable = g_object_ref (priv->cancellable); + GST_OBJECT_UNLOCK (self); + + buffer = gst_adapter_take_buffer (priv->input_adapter, size); + success = CLAPPER_URI_BASE_DEMUX_GET_CLASS (self)->process_buffer (self, buffer, cancellable); + gst_buffer_unref (buffer); + g_object_unref (cancellable); + + if (success) { + gst_event_unref (event); + return TRUE; + } + break; + } + case GST_EVENT_CUSTOM_DOWNSTREAM_STICKY:{ + ClapperUriBaseDemuxClass *uri_bd_class = CLAPPER_URI_BASE_DEMUX_GET_CLASS (self); + + if (uri_bd_class->handle_custom_event) + uri_bd_class->handle_custom_event (self, event); + + break; + } + default: + break; + } + + return gst_pad_event_default (pad, parent, event); +} + +static GstFlowReturn +clapper_uri_base_demux_sink_chain (GstPad *pad, GstObject *parent, GstBuffer *buffer) +{ + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (parent); + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + + gst_adapter_push (priv->input_adapter, buffer); + GST_DEBUG_OBJECT (self, "Received buffer, total collected: %" G_GSIZE_FORMAT " bytes", + gst_adapter_available (priv->input_adapter)); + + return GST_FLOW_OK; +} + +static void +clapper_uri_base_demux_init (ClapperUriBaseDemux *self) +{ + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + + priv->input_adapter = gst_adapter_new (); + priv->cancellable = g_cancellable_new (); +} + +static void +clapper_uri_base_demux_constructed (GObject *object) +{ + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (object); + GstPad *sink_pad; + + sink_pad = gst_pad_new_from_template (gst_element_class_get_pad_template ( + GST_ELEMENT_GET_CLASS (self), "sink"), "sink"); + gst_pad_set_event_function (sink_pad, + GST_DEBUG_FUNCPTR (clapper_uri_base_demux_sink_event)); + gst_pad_set_chain_function (sink_pad, + GST_DEBUG_FUNCPTR (clapper_uri_base_demux_sink_chain)); + + gst_pad_set_active (sink_pad, TRUE); + + if (!gst_element_add_pad (GST_ELEMENT_CAST (self), sink_pad)) + g_critical ("Failed to add sink pad to bin"); + + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static void +clapper_uri_base_demux_finalize (GObject *object) +{ + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (object); + ClapperUriBaseDemuxPrivate *priv = clapper_uri_base_demux_get_instance_private (self); + + g_object_unref (priv->input_adapter); + g_object_unref (priv->cancellable); + gst_clear_object (&priv->typefind_src); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_uri_base_demux_class_init (ClapperUriBaseDemuxClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + GstElementClass *gstelement_class = (GstElementClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperuribasedemux", 0, + "Clapper URI Base Demux"); + + gobject_class->constructed = clapper_uri_base_demux_constructed; + gobject_class->finalize = clapper_uri_base_demux_finalize; + + gstelement_class->change_state = clapper_uri_base_demux_change_state; + + gst_element_class_add_static_pad_template (gstelement_class, &src_template); +} diff --git a/src/lib/clapper/gst/clapper-uri-list-demux.c b/src/lib/clapper/gst/clapper-uri-list-demux.c deleted file mode 100644 index daf13ebc..00000000 --- a/src/lib/clapper/gst/clapper-uri-list-demux.c +++ /dev/null @@ -1,461 +0,0 @@ -/* Clapper Playback Library - * Copyright (C) 2024 Rafał Dzięgiel - * - * 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 - * . - */ - -#include - -#include "clapper-uri-list-demux-private.h" - -#define GST_CAT_DEFAULT clapper_uri_list_demux_debug -GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); - -struct _ClapperUriListDemux -{ - GstBin parent; - - GMutex lock; - - GstAdapter *input_adapter; - - GstElement *uri_handler; - GstElement *typefind; - - GstPad *typefind_src; - - GstStructure *http_headers; -}; - -static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", - GST_PAD_SINK, - GST_PAD_ALWAYS, - GST_STATIC_CAPS ("text/uri-list, source=(string)clapper-harvest")); - -static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", - GST_PAD_SRC, - GST_PAD_SOMETIMES, - GST_STATIC_CAPS_ANY); - -#define parent_class clapper_uri_list_demux_parent_class -G_DEFINE_TYPE (ClapperUriListDemux, clapper_uri_list_demux, GST_TYPE_BIN); -GST_ELEMENT_REGISTER_DEFINE (clapperurilistdemux, "clapperurilistdemux", - 512, CLAPPER_TYPE_URI_LIST_DEMUX); - -static void -_set_property (GstObject *obj, const gchar *prop_name, gpointer value) -{ - g_object_set (G_OBJECT (obj), prop_name, value, NULL); - - if (gst_debug_category_get_threshold (GST_CAT_DEFAULT) >= GST_LEVEL_DEBUG) { - gchar *el_name; - - el_name = gst_object_get_name (obj); - GST_DEBUG ("Set %s %s", el_name, prop_name); - - g_free (el_name); - } -} - -static gboolean -configure_deep_element (GQuark field_id, const GValue *value, GstElement *child) -{ - GObjectClass *gobject_class; - const GstStructure *substructure; - - if (!GST_VALUE_HOLDS_STRUCTURE (value)) - return TRUE; - - substructure = gst_value_get_structure (value); - - if (!gst_structure_has_name (substructure, "request-headers")) - return TRUE; - - gobject_class = G_OBJECT_GET_CLASS (child); - - if (g_object_class_find_property (gobject_class, "user-agent")) { - const gchar *ua; - - if ((ua = gst_structure_get_string (substructure, "User-Agent"))) - _set_property (GST_OBJECT_CAST (child), "user-agent", (gchar *) ua); - } - - if (g_object_class_find_property (gobject_class, "extra-headers")) { - GstStructure *extra_headers; - - extra_headers = gst_structure_copy (substructure); - gst_structure_set_name (extra_headers, "extra-headers"); - gst_structure_remove_field (extra_headers, "User-Agent"); - - _set_property (GST_OBJECT_CAST (child), "extra-headers", extra_headers); - - gst_structure_free (extra_headers); - } - - return TRUE; -} - -static void -clapper_uri_list_demux_deep_element_added (GstBin *bin, GstBin *sub_bin, GstElement *child) -{ - if (GST_OBJECT_FLAG_IS_SET (child, GST_ELEMENT_FLAG_SOURCE)) { - ClapperUriListDemux *self = CLAPPER_URI_LIST_DEMUX_CAST (bin); - - g_mutex_lock (&self->lock); - - if (self->http_headers) { - gst_structure_foreach (self->http_headers, - (GstStructureForeachFunc) configure_deep_element, child); - } - - g_mutex_unlock (&self->lock); - } -} - -static gboolean -remove_sometimes_pad_cb (GstElement *element, GstPad *pad, ClapperUriListDemux *self) -{ - GstPadTemplate *template = gst_pad_get_pad_template (pad); - GstPadPresence presence = GST_PAD_TEMPLATE_PRESENCE (template); - - gst_object_unref (template); - - if (presence == GST_PAD_SOMETIMES) { - GST_DEBUG_OBJECT (self, "Removing src pad"); - - gst_pad_set_active (pad, FALSE); - - if (G_UNLIKELY (!gst_element_remove_pad (element, pad))) - g_critical ("Failed to remove pad from bin"); - } - - return TRUE; -} - -static void -clapper_uri_list_demux_reset (ClapperUriListDemux *self) -{ - GstElement *element = GST_ELEMENT_CAST (self); - - gst_element_foreach_pad (element, (GstElementForeachPadFunc) remove_sometimes_pad_cb, NULL); -} - -static GstStateChangeReturn -clapper_uri_list_demux_change_state (GstElement *element, GstStateChange transition) -{ - ClapperUriListDemux *self = CLAPPER_URI_LIST_DEMUX_CAST (element); - GstStateChangeReturn ret; - - ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); - if (ret == GST_STATE_CHANGE_FAILURE) - return ret; - - switch (transition) { - case GST_STATE_CHANGE_PAUSED_TO_READY: - clapper_uri_list_demux_reset (self); - break; - default: - break; - } - - return ret; -} - -static gboolean -_feature_filter (GstPluginFeature *feature, const gchar *search_proto) -{ - GstElementFactory *factory; - const gchar *const *protocols; - const gchar *feature_name; - guint i; - - if (!GST_IS_ELEMENT_FACTORY (feature)) - return FALSE; - - factory = GST_ELEMENT_FACTORY_CAST (feature); - - if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC) - return FALSE; - - feature_name = gst_plugin_feature_get_name (feature); - - /* Do not loop endlessly creating our own sources and demuxers */ - if (!feature_name || strcmp (feature_name, "clapperextractablesrc") == 0) - return FALSE; - - protocols = gst_element_factory_get_uri_protocols (factory); - - if (protocols) { - for (i = 0; protocols[i]; ++i) { - if (g_ascii_strcasecmp (protocols[i], search_proto) == 0) - return TRUE; - } - } - - return FALSE; -} - -static GstElement * -_make_handler_for_uri (ClapperUriListDemux *self, const gchar *uri) -{ - GstElement *element = NULL; - GList *factories, *f; - gchar *protocol; - - if (!gst_uri_is_valid (uri)) { - GST_ERROR_OBJECT (self, "Cannot create handler for invalid URI: \"%s\"", uri); - return NULL; - } - - protocol = gst_uri_get_protocol (uri); - factories = gst_registry_feature_filter (gst_registry_get (), - (GstPluginFeatureFilter) _feature_filter, FALSE, protocol); - g_free (protocol); - - factories = g_list_sort (factories, - (GCompareFunc) gst_plugin_feature_rank_compare_func); - - for (f = factories; f; f = g_list_next (f)) { - GstElementFactory *factory = f->data; - - if ((element = gst_element_factory_create (factory, NULL)) - && gst_uri_handler_set_uri (GST_URI_HANDLER (element), uri, NULL)) - break; - - gst_clear_object (&element); - } - - gst_plugin_feature_list_free (factories); - - GST_DEBUG_OBJECT (self, "Created URI handler: %s", - GST_OBJECT_NAME (element)); - - return element; -} - -static gboolean -clapper_uri_list_demux_process_buffer (ClapperUriListDemux *self, GstBuffer *buffer) -{ - GstMemory *mem; - GstMapInfo info; - - mem = gst_buffer_peek_memory (buffer, 0); - - if (mem && gst_memory_map (mem, &info, GST_MAP_READ)) { - GstPad *uri_handler_src, *typefind_sink, *src_ghostpad; - GstPadLinkReturn pad_link_ret; - - GST_DEBUG_OBJECT (self, "Stream URI: %s", (const gchar *) info.data); - - if (self->uri_handler) { - GST_DEBUG_OBJECT (self, "Trying to reuse existing URI handler"); - - if (gst_uri_handler_set_uri (GST_URI_HANDLER (self->uri_handler), - (const gchar *) info.data, NULL)) { - GST_DEBUG_OBJECT (self, "Reused existing URI handler"); - } else { - GST_DEBUG_OBJECT (self, "Could not reuse existing URI handler"); - - if (self->typefind_src) { - gst_element_remove_pad (GST_ELEMENT_CAST (self), self->typefind_src); - gst_clear_object (&self->typefind_src); - } - - gst_bin_remove (GST_BIN_CAST (self), self->uri_handler); - gst_bin_remove (GST_BIN_CAST (self), self->typefind); - - self->uri_handler = NULL; - self->typefind = NULL; - } - } - - if (!self->uri_handler) { - GST_DEBUG_OBJECT (self, "Creating new URI handler element"); - - self->uri_handler = _make_handler_for_uri (self, (const gchar *) info.data); - - if (G_UNLIKELY (!self->uri_handler)) { - GST_ERROR_OBJECT (self, "Could not create URI handler element"); - - GST_ELEMENT_ERROR (self, CORE, MISSING_PLUGIN, - ("Missing plugin to handle URI: %s", info.data), (NULL)); - gst_memory_unmap (mem, &info); - - return FALSE; - } - - gst_bin_add (GST_BIN_CAST (self), self->uri_handler); - - self->typefind = gst_element_factory_make ("typefind", NULL); - gst_bin_add (GST_BIN_CAST (self), self->typefind); - - uri_handler_src = gst_element_get_static_pad (self->uri_handler, "src"); - typefind_sink = gst_element_get_static_pad (self->typefind, "sink"); - - pad_link_ret = gst_pad_link_full (uri_handler_src, typefind_sink, - GST_PAD_LINK_CHECK_NOTHING); - - if (pad_link_ret != GST_PAD_LINK_OK) - g_critical ("Failed to link bin elements"); - - g_object_unref (uri_handler_src); - g_object_unref (typefind_sink); - - self->typefind_src = gst_element_get_static_pad (self->typefind, "src"); - - src_ghostpad = gst_ghost_pad_new_from_template ("src", self->typefind_src, - gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (self), "src")); - - gst_pad_set_active (src_ghostpad, TRUE); - - if (!gst_element_add_pad (GST_ELEMENT_CAST (self), src_ghostpad)) { - g_critical ("Failed to add source pad to bin"); - } else { - GST_DEBUG_OBJECT (self, "Added src pad, signalling \"no-more-pads\""); - gst_element_no_more_pads (GST_ELEMENT_CAST (self)); - } - } - - gst_memory_unmap (mem, &info); - - gst_element_sync_state_with_parent (self->typefind); - gst_element_sync_state_with_parent (self->uri_handler); - } - - return TRUE; -} - -static gboolean -clapper_uri_list_demux_sink_event (GstPad *pad, GstObject *parent, GstEvent *event) -{ - ClapperUriListDemux *self = CLAPPER_URI_LIST_DEMUX_CAST (parent); - - switch (GST_EVENT_TYPE (event)) { - case GST_EVENT_EOS:{ - GstBuffer *buffer; - gsize size; - gboolean success; - - size = gst_adapter_available (self->input_adapter); - - if (size == 0) { - GST_WARNING_OBJECT (self, "Received EOS without URI data"); - break; - } - - buffer = gst_adapter_take_buffer (self->input_adapter, size); - success = clapper_uri_list_demux_process_buffer (self, buffer); - gst_buffer_unref (buffer); - - if (success) { - gst_event_unref (event); - return TRUE; - } - break; - } - case GST_EVENT_CUSTOM_DOWNSTREAM_STICKY:{ - const GstStructure *structure = gst_event_get_structure (event); - - if (structure && gst_structure_has_name (structure, "http-headers")) { - GST_DEBUG_OBJECT (self, "Received \"http-headers\" custom event"); - g_mutex_lock (&self->lock); - - gst_clear_structure (&self->http_headers); - self->http_headers = gst_structure_copy (structure); - - g_mutex_unlock (&self->lock); - } - break; - } - default: - break; - } - - return gst_pad_event_default (pad, parent, event); -} - -static GstFlowReturn -clapper_uri_list_demux_sink_chain (GstPad *pad, GstObject *parent, GstBuffer *buffer) -{ - ClapperUriListDemux *self = CLAPPER_URI_LIST_DEMUX_CAST (parent); - - gst_adapter_push (self->input_adapter, buffer); - GST_DEBUG_OBJECT (self, "Received buffer, total collected: %" G_GSIZE_FORMAT " bytes", - gst_adapter_available (self->input_adapter)); - - return GST_FLOW_OK; -} - -static void -clapper_uri_list_demux_init (ClapperUriListDemux *self) -{ - GstPad *sink_pad; - - g_mutex_init (&self->lock); - - self->input_adapter = gst_adapter_new (); - - sink_pad = gst_pad_new_from_template (gst_element_class_get_pad_template ( - GST_ELEMENT_GET_CLASS (self), "sink"), "sink"); - gst_pad_set_event_function (sink_pad, - GST_DEBUG_FUNCPTR (clapper_uri_list_demux_sink_event)); - gst_pad_set_chain_function (sink_pad, - GST_DEBUG_FUNCPTR (clapper_uri_list_demux_sink_chain)); - - gst_pad_set_active (sink_pad, TRUE); - - if (!gst_element_add_pad (GST_ELEMENT_CAST (self), sink_pad)) - g_critical ("Failed to add sink pad to bin"); -} - -static void -clapper_uri_list_demux_finalize (GObject *object) -{ - ClapperUriListDemux *self = CLAPPER_URI_LIST_DEMUX_CAST (object); - - GST_TRACE_OBJECT (self, "Finalize"); - - g_object_unref (self->input_adapter); - gst_clear_object (&self->typefind_src); - gst_clear_structure (&self->http_headers); - - g_mutex_clear (&self->lock); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -static void -clapper_uri_list_demux_class_init (ClapperUriListDemuxClass *klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - GstElementClass *gstelement_class = (GstElementClass *) klass; - GstBinClass *gstbin_class = (GstBinClass *) klass; - - GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperurilistdemux", 0, - "Clapper URI List Demux"); - - gobject_class->finalize = clapper_uri_list_demux_finalize; - - gstbin_class->deep_element_added = clapper_uri_list_demux_deep_element_added; - - gstelement_class->change_state = clapper_uri_list_demux_change_state; - - gst_element_class_add_static_pad_template (gstelement_class, &sink_template); - gst_element_class_add_static_pad_template (gstelement_class, &src_template); - - gst_element_class_set_static_metadata (gstelement_class, "Clapper URI List Demux", - "Demuxer", "A custom demuxer for URI lists", - "Rafał Dzięgiel "); -} diff --git a/src/lib/clapper/meson.build b/src/lib/clapper/meson.build index 655ea4df..41eb3fa4 100644 --- a/src/lib/clapper/meson.build +++ b/src/lib/clapper/meson.build @@ -119,6 +119,7 @@ clapper_headers = [ 'clapper-marker.h', 'clapper-media-item.h', 'clapper-player.h', + 'clapper-playlistable.h', 'clapper-queue.h', 'clapper-reactable.h', 'clapper-stream.h', @@ -147,6 +148,7 @@ clapper_sources = [ 'clapper-media-item.c', 'clapper-playbin-bus.c', 'clapper-player.c', + 'clapper-playlistable.c', 'clapper-queue.c', 'clapper-reactable.c', 'clapper-reactables-manager.c', @@ -160,7 +162,9 @@ clapper_sources = [ 'gst/clapper-plugin.c', 'gst/clapper-extractable-src.c', 'gst/clapper-enhancer-director.c', - 'gst/clapper-uri-list-demux.c', + 'gst/clapper-uri-base-demux.c', + 'gst/clapper-harvest-uri-demux.c', + 'gst/clapper-playlist-demux.c', '../shared/clapper-shared-utils.c', ] clapper_c_args = [