18 Commits

Author SHA1 Message Date
Rafał Dzięgiel
4d5519b42d Merge pull request #575 from Rafostar/playlistable
Expandable playlists support
2025-07-26 18:10:29 +02:00
Rafał Dzięgiel
4a34fb3484 clapper: discoverer: Skip items that already have tags
When tags are populated elsewhere, do not run discovery on them again.
It is possible they were discovered in more efficient maner
(e.g. from playlist data itself).

This avoid us downloading each media item separately after all
playlist items are queued.
2025-07-26 15:37:33 +02:00
Rafał Dzięgiel
d8ea220aad clapper-app: Remove claps handling code
It is now handled inside playlist demuxer, with the other playlist formats
2025-07-26 15:37:31 +02:00
Rafał Dzięgiel
dc56cb201a clapper: Support demuxing uri-list and claps files 2025-07-26 15:37:28 +02:00
Rafał Dzięgiel
07f944fb31 clapper: Update harvest common formats description 2025-07-26 15:37:26 +02:00
Rafał Dzięgiel
b2e6533c30 clapper: Store full caps in harvest
Make it possible to know which enhancer harvested this cached data
2025-07-26 15:37:24 +02:00
Rafał Dzięgiel
7a56fbfff8 clapper: Rename "uri-list-demux" element
Move as harvest URI demuxer since it is supposed to work only with
harvest data from extractable src. Also change caps media type
to "text/x-uri" which is non-standard, but we have to differentiate
single URI from harvest and URI list (one or more URIs).
2025-07-26 15:37:22 +02:00
Rafał Dzięgiel
7457ffda13 clapper: Handle parsed playlists
Handle "ClapperPlaylistParsed" messages on playbin bus by updating current media
item (playlist) to redirect to the first item in that playlist (with changed tags)
and appending remaining items to the queue after that playlist position.

This basically means that playlist gets resolved into simply adding more
items to the queue. This should also work with nested playlists within playlist.
2025-07-26 15:37:18 +02:00
Rafał Dzięgiel
bee2e08fb1 clapper: Add playlist demuxer element
Uses "Playlistable" enhancers to parse playlist and demux first URI in it
2025-07-26 15:32:32 +02:00
Rafał Dzięgiel
0c1d291006 clapper: Split URI list demux code into subclass 2025-07-19 15:43:36 +02:00
Rafał Dzięgiel
4002a63e3a clapper: Add "Playlistable" interface
An interface for creating enhancers that parse data
into individual media items
2025-07-19 15:43:30 +02:00
Rafał Dzięgiel
ff054743e6 flatpak: Sync with Flathub 2025-07-19 15:39:42 +02:00
Rafał Dzięgiel
9432156aec flatpak: Sync with Flathub 2025-07-19 14:54:02 +02:00
Rafał Dzięgiel
47d3ebe693 Revert "flatpak: Skip libmpeg2 build for now"
This reverts commit f63e13ed39.

This workaround does not work, since GStreamer build manifest we use
as shared module from Flathub has explicitly enabled libmpeg2 support.
2025-07-19 12:56:13 +02:00
Rafał Dzięgiel
5f8270f0e8 clapper-app: Retain compatibility with older graphviz 2025-07-19 12:49:01 +02:00
Rafał Dzięgiel
54f059aaa3 clapper-app: Correct pipeline SVG size variable type 2025-07-19 12:03:52 +02:00
Rafał Dzięgiel
f63e13ed39 flatpak: Skip libmpeg2 build for now
Source URI is dead. Build Flatpak without it until
fixed or mirror is found, so our CI works again.
2025-07-19 11:59:22 +02:00
Rafał Dzięgiel
daadabba8d Merge pull request #567 from Rafostar/mpris-compat
MPRIS compat changes
2025-06-26 19:44:07 +02:00
34 changed files with 1844 additions and 602 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

@@ -582,7 +582,7 @@ _create_pipeline_svg_file_in_thread (GTask *task, GObject *source G_GNUC_UNUSED,
GVC_t *gvc;
gchar *path, *template = NULL, *dot_data = NULL, *img_data = NULL;
gint fd;
guint size = 0;
gsize size = 0;
if (!(tmp_subdir = _create_tmp_subdir ("pipelines", cancellable, &error)))
goto finish;
@@ -610,7 +610,16 @@ _create_pipeline_svg_file_in_thread (GTask *task, GObject *source G_GNUC_UNUSED,
gvc = gvContext ();
gvLayout (gvc, graph, "dot");
#ifdef HAVE_GVC_13
gvRenderData (gvc, graph, "svg", &img_data, &size);
#else
{
guint tmp_size = 0; // Temporary uint to satisfy older API
gvRenderData (gvc, graph, "svg", &img_data, &tmp_size);
size = tmp_size;
}
#endif
agclose (graph);
gvFreeContext (gvc);

View File

@@ -105,6 +105,9 @@ if not pp_option.disabled()
if cgraph_dep.found() and gvc_dep.found()
clapperapp_c_args += ['-DHAVE_GRAPHVIZ']
clapperapp_deps += [cgraph_dep, gvc_dep]
if gvc_dep.version().version_compare('>= 13.0.0')
clapperapp_c_args += ['-DHAVE_GVC_13']
endif
clapperapp_available_functionalities += 'pipeline-preview'
elif pp_option.enabled()
error('pipeline-preview option was enabled, but required dependencies were not found')

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