Merge pull request #575 from Rafostar/playlistable

Expandable playlists support
This commit is contained in:
Rafał Dzięgiel
2025-07-26 18:10:29 +02:00
committed by GitHub
31 changed files with 1830 additions and 600 deletions

View File

@@ -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: [

View File

@@ -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);

View File

@@ -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);

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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:
*

View File

@@ -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;

View File

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

View File

@@ -0,0 +1,31 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <glib.h>
#include <gio/gio.h>
#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

View File

@@ -0,0 +1,59 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
/**
* ClapperPlaylistable:
*
* An interface for creating enhancers that parse data into individual media items.
*
* Since: 0.10
*/
#include <gst/gst.h>
#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);
}

View File

@@ -0,0 +1,71 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#pragma once
#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION)
#error "Only <clapper/clapper.h> can be included directly."
#endif
#include <glib.h>
#include <glib-object.h>
#include <gio/gio.h>
#include <clapper/clapper-visibility.h>
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

View File

@@ -19,6 +19,7 @@
#pragma once
#include <glib.h>
#include <gio/gio.h>
#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);

View File

@@ -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);
}

View File

@@ -44,6 +44,7 @@
#include <clapper/clapper-video-stream.h>
#include <clapper/clapper-extractable.h>
#include <clapper/clapper-playlistable.h>
#include <clapper/clapper-reactable.h>
#include <clapper/clapper-functionalities-availability.h>

View File

@@ -39,6 +39,7 @@
*/
#include <gst/gst.h>
#include <gst/tag/tag.h>
#include <gst/pbutils/pbutils.h>
#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);

View File

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

View File

@@ -25,7 +25,9 @@
#include "../clapper-cache-private.h"
#include "../clapper-enhancer-proxy-private.h"
#include "../clapper-extractable-private.h"
#include "../clapper-playlistable-private.h"
#include "../clapper-harvest-private.h"
#include "../clapper-media-item.h"
#include "../clapper-utils.h"
#include "../../shared/clapper-shared-utils-private.h"
@@ -53,6 +55,7 @@ typedef struct
ClapperEnhancerDirector *director;
GList *filtered_proxies;
GUri *uri;
GstBuffer *buffer;
GCancellable *cancellable;
GError **error;
} ClapperEnhancerDirectorData;
@@ -99,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)
{

View File

@@ -1,5 +1,5 @@
/* Clapper Playback Library
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -21,16 +21,17 @@
#include <glib.h>
#include <glib-object.h>
#include <gst/gst.h>
#include <gst/gstbin.h>
#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

View File

@@ -0,0 +1,190 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#include "clapper-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 <rafostar.github@gmail.com>");
}

View File

@@ -0,0 +1,38 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <glib.h>
#include <glib-object.h>
#include <gst/gst.h>
#include "clapper-uri-base-demux-private.h"
G_BEGIN_DECLS
#define CLAPPER_TYPE_PLAYLIST_DEMUX (clapper_playlist_demux_get_type())
#define CLAPPER_PLAYLIST_DEMUX_CAST(obj) ((ClapperPlaylistDemux *)(obj))
G_GNUC_INTERNAL
G_DECLARE_FINAL_TYPE (ClapperPlaylistDemux, clapper_playlist_demux, CLAPPER, PLAYLIST_DEMUX, ClapperUriBaseDemux)
GST_TYPE_FIND_REGISTER_DECLARE (clapperplaylistdemux)
GST_ELEMENT_REGISTER_DECLARE (clapperplaylistdemux)
G_END_DECLS

View File

@@ -0,0 +1,514 @@
/* Clapper Playback Library
* Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#include "clapper-playlist-demux-private.h"
#include "clapper-enhancer-director-private.h"
#include "../clapper-basic-functions.h"
#include "../clapper-enhancer-proxy.h"
#include "../clapper-enhancer-proxy-list.h"
#include "../clapper-media-item.h"
#include "../clapper-playlistable.h"
#define CLAPPER_PLAYLIST_MEDIA_TYPE "application/clapper-playlist"
#define 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 (&reg_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 (&reg_caps);
return res;
}
#define parent_class clapper_playlist_demux_parent_class
G_DEFINE_TYPE (ClapperPlaylistDemux, clapper_playlist_demux, CLAPPER_TYPE_URI_BASE_DEMUX);
GST_TYPE_FIND_REGISTER_DEFINE_CUSTOM (clapperplaylistdemux, type_find_register);
GST_ELEMENT_REGISTER_DEFINE (clapperplaylistdemux, "clapperplaylistdemux",
512, CLAPPER_TYPE_PLAYLIST_DEMUX);
static 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 <rafostar.github@gmail.com>");
}

View File

@@ -24,15 +24,17 @@
#include "../clapper-enhancer-proxy.h"
#include "../clapper-enhancer-proxy-list-private.h"
#include "../clapper-extractable.h"
#include "../clapper-playlistable.h"
#include "clapper-plugin-private.h"
#include "clapper-extractable-src-private.h"
#include "clapper-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;
}

View File

@@ -0,0 +1,48 @@
/* Clapper Playback Library
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <glib.h>
#include <glib-object.h>
#include <gio/gio.h>
#include <gst/gst.h>
#include <gst/gstbin.h>
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

View File

@@ -0,0 +1,408 @@
/* Clapper Playback Library
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#include <gst/base/gstadapter.h>
#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);
}

View File

@@ -1,461 +0,0 @@
/* Clapper Playback Library
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*/
#include <gst/base/gstadapter.h>
#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 <rafostar.github@gmail.com>");
}

View File

@@ -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 = [