Merge pull request #594 from Rafostar/devel

Playlists handling changes and bugfixes
This commit is contained in:
Rafał Dzięgiel
2025-11-23 19:28:14 +01:00
committed by GitHub
18 changed files with 569 additions and 140 deletions

View File

@@ -1292,9 +1292,13 @@ clapper_app_window_constructed (GObject *object)
#endif
#if CLAPPER_HAVE_DISCOVERER
feature = CLAPPER_FEATURE (clapper_discoverer_new ());
clapper_player_add_feature (player, feature);
gst_object_unref (feature);
if ((proxy = clapper_enhancer_proxy_list_get_proxy_by_module (proxies, "clapper-media-scanner"))) {
gst_object_unref (proxy);
} else {
feature = CLAPPER_FEATURE (clapper_discoverer_new ());
clapper_player_add_feature (player, feature);
gst_object_unref (feature);
}
#endif
/* FIXME: Allow setting sink/filter elements from prefs window

View File

@@ -54,6 +54,21 @@ typedef enum
CLAPPER_PLAYER_SEEK_METHOD_FAST,
} ClapperPlayerSeekMethod;
/**
* ClapperPlayerMessageDestination:
* @CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER: Messaging from application or reactable enhancers to the player itself.
* @CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES: Messaging from application to the reactable enhancers.
* @CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION: Messaging from reactable enhancers to the application.
*
* Since: 0.10
*/
typedef enum
{
CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER = 0,
CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES,
CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION,
} ClapperPlayerMessageDestination;
/**
* ClapperQueueProgressionMode:
* @CLAPPER_QUEUE_PROGRESSION_NONE: Queue will not change current item after playback finishes.
@@ -119,6 +134,8 @@ typedef enum
* @CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT: Only run discovery on an item if it is not a currently selected item in [class@Clapper.Queue].
* This mode is optimal when application always plays (or at least goes into paused) after selecting item from queue.
* It will skip discovery of such items since they will be discovered by [class@Clapper.Player] anyway.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
typedef enum
{
@@ -156,6 +173,8 @@ typedef enum
* @CLAPPER_REACTABLE_ITEM_UPDATED_DURATION: Media item duration was updated.
* @CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE: Media item timeline was updated.
* @CLAPPER_REACTABLE_ITEM_UPDATED_TAGS: Media item tags were updated.
* @CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI: Media item redirect URI was updated.
* @CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION: Media item cache location was updated.
*
* Flags informing which properties were updated within [class@Clapper.MediaItem].
*
@@ -167,6 +186,8 @@ typedef enum
CLAPPER_REACTABLE_ITEM_UPDATED_DURATION = 1 << 1,
CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE = 1 << 2,
CLAPPER_REACTABLE_ITEM_UPDATED_TAGS = 1 << 3,
CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI = 1 << 4,
CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION = 1 << 5,
} ClapperReactableItemUpdatedFlags;
G_END_DECLS

View File

@@ -34,7 +34,7 @@ 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);
gboolean clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *item, GListStore *playlist, GstObject *playlist_src, ClapperPlayer *player);
G_GNUC_INTERNAL
gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus);

View File

@@ -54,7 +54,8 @@ struct _ClapperMediaItem
/* Whether using title from URI */
gboolean title_is_parsed;
GSList *redirects;
GType redirects_src_type;
gchar *redirect_uri;
gchar *cache_uri;
/* For shuffle */
@@ -74,6 +75,7 @@ enum
PROP_ID,
PROP_URI,
PROP_SUBURI,
PROP_REDIRECT_URI,
PROP_CACHE_LOCATION,
PROP_TAGS,
PROP_TITLE,
@@ -202,12 +204,6 @@ 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
@@ -277,6 +273,55 @@ clapper_media_item_get_suburi (ClapperMediaItem *self)
return suburi;
}
/**
* clapper_media_item_get_redirect_uri:
* @item: a #ClapperMediaItem
*
* Get permanent redirect URI of #ClapperMediaItem.
*
* Returns: (transfer full) (nullable): a redirected URI of #ClapperMediaItem.
*
* Since: 0.10
*/
gchar *
clapper_media_item_get_redirect_uri (ClapperMediaItem *self)
{
gchar *redirect_uri;
g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL);
GST_OBJECT_LOCK (self);
redirect_uri = g_strdup (self->redirect_uri);
GST_OBJECT_UNLOCK (self);
return redirect_uri;
}
/**
* clapper_media_item_get_cache_location:
* @item: a #ClapperMediaItem
*
* Get downloaded cache file location of #ClapperMediaItem.
*
* Returns: (transfer full) (type filename) (nullable): a cache file location of #ClapperMediaItem.
*
* Since: 0.10
*/
gchar *
clapper_media_item_get_cache_location (ClapperMediaItem *self)
{
gchar *cache_location = NULL;
g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL);
GST_OBJECT_LOCK (self);
if (self->cache_uri)
cache_location = g_filename_from_uri (self->cache_uri, NULL, NULL);
GST_OBJECT_UNLOCK (self);
return cache_location;
}
/**
* clapper_media_item_get_title:
* @item: a #ClapperMediaItem
@@ -681,49 +726,85 @@ clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDisco
/* XXX: Must be set from player thread */
static inline gboolean
clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri)
clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri,
GstObject *redirect_src)
{
/* 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))
GType src_type;
gboolean changed;
/* Safety checks */
if (G_UNLIKELY (!redirect_uri)) {
GST_ERROR_OBJECT (self, "Received redirect request without an URI set");
return FALSE;
}
if (G_UNLIKELY (!redirect_src)) {
GST_ERROR_OBJECT (self, "Received redirect request without source object set");
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);
/* Only allow redirects determined by the same source type, otherwise
* we would start mixing URIs when multiple sources message them */
src_type = G_OBJECT_TYPE (redirect_src);
return TRUE;
if (self->redirects_src_type == 0) {
self->redirects_src_type = src_type;
} else if (self->redirects_src_type != src_type) {
GST_LOG_OBJECT (self, "Ignoring redirection from different source: %s",
G_OBJECT_TYPE_NAME (redirect_src));
return FALSE;
}
GST_OBJECT_LOCK (self);
changed = g_set_str (&self->redirect_uri, redirect_uri);
GST_OBJECT_UNLOCK (self);
GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\", source: %s",
redirect_uri, G_OBJECT_TYPE_NAME (redirect_src));
return changed;
}
gboolean
clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *other_item,
ClapperPlayer *player)
clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListStore *playlist,
GstObject *playlist_src, ClapperPlayer *player)
{
gboolean title_changed = FALSE;
ClapperMediaItem *other_item;
const gchar *redirect_uri;
gboolean success, title_changed = FALSE;
if (!clapper_media_item_set_redirect_uri (self, clapper_media_item_get_uri (other_item)))
return FALSE;
/* First playlist item URI becomes a redirect URI for item to be updated */
other_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0);
redirect_uri = clapper_media_item_get_uri (other_item);
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;
if ((success = clapper_media_item_set_redirect_uri (self, redirect_uri, playlist_src))) {
ClapperFeaturesManager *features_manager;
ClapperReactableItemUpdatedFlags flags = CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI;
clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]);
/* Notify about URI change before other properties */
clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self),
param_specs[PROP_REDIRECT_URI]);
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) {
flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TITLE;
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);
@@ -731,20 +812,51 @@ clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *o
clapper_features_manager_trigger_item_updated (features_manager, self);
}
return TRUE;
gst_object_unref (other_item);
return success;
}
/* XXX: Must be set from player thread or upon construction */
void
clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location)
{
g_clear_pointer (&self->cache_uri, g_free);
gboolean changed;
if (location)
self->cache_uri = g_filename_to_uri (location, NULL, NULL);
GST_OBJECT_LOCK (self);
GST_DEBUG_OBJECT (self, "Set cache URI: \"%s\"",
GST_STR_NULL (self->cache_uri));
/* Skip if both are NULL (no change - called during construction) */
if ((changed = (self->cache_uri || location))) {
g_clear_pointer (&self->cache_uri, g_free);
if (location)
self->cache_uri = g_filename_to_uri (location, NULL, NULL);
GST_DEBUG_OBJECT (self, "Set cache URI: \"%s\"",
GST_STR_NULL (self->cache_uri));
}
GST_OBJECT_UNLOCK (self);
if (changed) {
ClapperPlayer *player;
if ((player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) {
ClapperFeaturesManager *features_manager;
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CACHE_LOCATION]);
if (player->reactables_manager) {
clapper_reactables_manager_trigger_item_updated (player->reactables_manager, self,
CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION);
}
if ((features_manager = clapper_player_get_features_manager (player)))
clapper_features_manager_trigger_item_updated (features_manager, self);
gst_object_unref (player);
}
}
}
/* XXX: Can only be read from player thread.
@@ -763,13 +875,10 @@ clapper_media_item_get_playback_uri (ClapperMediaItem *self)
if (exists)
return self->cache_uri;
/* Do not test file existence next time */
clapper_media_item_set_cache_location (self, NULL);
}
if (self->redirects)
return self->redirects->data;
if (self->redirect_uri)
return self->redirect_uri;
return self->uri;
}
@@ -814,7 +923,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);
self->title_is_parsed = TRUE;
G_OBJECT_CLASS (parent_class)->constructed (object);
}
@@ -835,7 +944,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->redirect_uri);
g_free (self->cache_uri);
G_OBJECT_CLASS (parent_class)->finalize (object);
@@ -879,6 +988,12 @@ clapper_media_item_get_property (GObject *object, guint prop_id,
case PROP_SUBURI:
g_value_take_string (value, clapper_media_item_get_suburi (self));
break;
case PROP_REDIRECT_URI:
g_value_take_string (value, clapper_media_item_get_redirect_uri (self));
break;
case PROP_CACHE_LOCATION:
g_value_take_string (value, clapper_media_item_get_cache_location (self));
break;
case PROP_TAGS:
g_value_take_boxed (value, clapper_media_item_get_tags (self));
break;
@@ -940,16 +1055,40 @@ clapper_media_item_class_init (ClapperMediaItemClass *klass)
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperMediaItem:redirect-uri:
*
* Media permanent redirect URI.
*
* Changes when player determines a new redirect for given media item.
* This will also happen when item URI leads to a playlist. Once playlist
* is parsed, item is merged with the first item on that playlist and the
* remaining items are appended to the playback queue after that item position.
*
* Once redirect URI in item is present, player will use that URI instead
* of the default one. Cache location takes precedence over both URIs through.
*/
param_specs[PROP_REDIRECT_URI] = g_param_spec_string ("redirect-uri",
NULL, NULL, NULL,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperMediaItem:cache-location:
*
* Media downloaded cache file location.
*
* This can be either set for newly created media items or
* it will be updated after download is completed if
* [property@Clapper.Player:download-enabled] is set.
*
* NOTE: This property was added in 0.8 as write at construct only.
* It can also be read only since Clapper 0.10.
*
* Since: 0.8
*/
param_specs[PROP_CACHE_LOCATION] = g_param_spec_string ("cache-location",
NULL, NULL, NULL,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperMediaItem:tags:

View File

@@ -60,6 +60,12 @@ void clapper_media_item_set_suburi (ClapperMediaItem *item, const gchar *suburi)
CLAPPER_API
gchar * clapper_media_item_get_suburi (ClapperMediaItem *item);
CLAPPER_API
gchar * clapper_media_item_get_redirect_uri (ClapperMediaItem *item);
CLAPPER_API
gchar * clapper_media_item_get_cache_location (ClapperMediaItem *item);
CLAPPER_API
gchar * clapper_media_item_get_title (ClapperMediaItem *item);

View File

@@ -49,4 +49,6 @@ void clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem
void clapper_playbin_bus_post_item_suburi_change (GstBus *bus, ClapperMediaItem *item);
void clapper_playbin_bus_post_user_message (GstBus *bus, GstMessage *msg);
G_END_DECLS

View File

@@ -29,6 +29,7 @@
#include "clapper-stream-private.h"
#include "clapper-stream-list-private.h"
#include "gst/clapper-extractable-src-private.h"
#include "gst/clapper-playlist-demux-private.h"
#define GST_CAT_DEFAULT clapper_playbin_bus_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
@@ -42,7 +43,8 @@ enum
CLAPPER_PLAYBIN_BUS_STRUCTURE_RATE_CHANGE,
CLAPPER_PLAYBIN_BUS_STRUCTURE_STREAM_CHANGE,
CLAPPER_PLAYBIN_BUS_STRUCTURE_CURRENT_ITEM_CHANGE,
CLAPPER_PLAYBIN_BUS_STRUCTURE_ITEM_SUBURI_CHANGE
CLAPPER_PLAYBIN_BUS_STRUCTURE_ITEM_SUBURI_CHANGE,
CLAPPER_PLAYBIN_BUS_STRUCTURE_USER_MESSAGE
};
static ClapperBusQuark _structure_quarks[] = {
@@ -54,6 +56,7 @@ static ClapperBusQuark _structure_quarks[] = {
{"stream-change", 0},
{"current-item-change", 0},
{"item-suburi-change", 0},
{"user-message", 0},
{NULL, 0}
};
@@ -325,7 +328,7 @@ clapper_playbin_bus_post_set_play_flag (GstBus *bus,
ClapperPlayerPlayFlags flag, gboolean enabled)
{
GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SET_PLAY_FLAG),
_FIELD_QUARK (FLAG), G_TYPE_FLAGS, flag,
_FIELD_QUARK (FLAG), G_TYPE_UINT, flag,
_FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, enabled,
NULL);
gst_bus_post (bus, gst_message_new_application (NULL, structure));
@@ -336,10 +339,10 @@ _handle_set_play_flag_msg (GstMessage *msg, const GstStructure *structure, Clapp
{
ClapperPlayerPlayFlags flag = 0;
gboolean enabled, enable = FALSE;
gint flags = 0;
guint flags = 0;
gst_structure_id_get (structure,
_FIELD_QUARK (FLAG), G_TYPE_FLAGS, &flag,
_FIELD_QUARK (FLAG), G_TYPE_UINT, &flag,
_FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, &enable,
NULL);
@@ -402,7 +405,7 @@ clapper_playbin_bus_post_seek (GstBus *bus, gdouble position, ClapperPlayerSeekM
{
GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SEEK),
_FIELD_QUARK (POSITION), G_TYPE_INT64, (gint64) (position * GST_SECOND),
_FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, seek_method,
_FIELD_QUARK (SEEK_METHOD), G_TYPE_INT, seek_method,
NULL);
gst_bus_post (bus, gst_message_new_application (NULL, structure));
}
@@ -422,7 +425,7 @@ _handle_seek_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer
gst_structure_id_get (structure,
_FIELD_QUARK (POSITION), G_TYPE_INT64, &position,
_FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, &seek_method,
_FIELD_QUARK (SEEK_METHOD), G_TYPE_INT, &seek_method,
NULL);
/* If we are starting playback, do a seek after preroll */
@@ -646,7 +649,7 @@ clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem *cur
{
GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (CURRENT_ITEM_CHANGE),
_FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, current_item,
_FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, mode,
_FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_INT, mode,
NULL);
gst_bus_post (bus, gst_message_new_application (NULL, structure));
}
@@ -659,7 +662,7 @@ _handle_current_item_change_msg (GstMessage *msg, const GstStructure *structure,
gst_structure_id_get (structure,
_FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, &current_item,
_FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, &mode,
_FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_INT, &mode,
NULL);
player->pending_position = 0; // We store pending position for played item, so reset
@@ -793,6 +796,100 @@ _handle_stream_change_msg (GstMessage *msg,
}
}
void
clapper_playbin_bus_post_user_message (GstBus *bus, GstMessage *msg)
{
GstStructure *structure = gst_structure_new_id_empty (_STRUCTURE_QUARK (USER_MESSAGE));
GValue value = G_VALUE_INIT;
g_value_init (&value, GST_TYPE_MESSAGE);
g_value_take_boxed (&value, msg);
gst_structure_id_take_value (structure, _FIELD_QUARK (VALUE), &value);
gst_bus_post (bus, gst_message_new_application (NULL, structure));
}
static inline void
_on_playlist_parsed_msg (GstMessage *msg, ClapperPlayer *player)
{
GstObject *src = GST_MESSAGE_SRC (msg);
ClapperMediaItem *playlist_item = NULL;
GListStore *playlist = NULL;
const GstStructure *structure;
guint n_items;
if (G_UNLIKELY (!src)) {
GST_WARNING_OBJECT (player, "Ignoring playlist parsed message without a source");
return;
}
structure = gst_message_get_structure (msg);
/* 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 if (CLAPPER_IS_PLAYLIST_DEMUX (src)) {
GST_OBJECT_LOCK (player);
/* Playlist from demuxer 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)) {
gboolean updated;
/* Update redirect URI (must be done from player thread) */
updated = clapper_media_item_update_from_parsed_playlist (playlist_item, playlist, src, player);
if (updated && 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);
}
static inline void
_handle_user_message_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player)
{
GstMessage *user_message = NULL;
gst_structure_id_get (structure,
_FIELD_QUARK (VALUE), GST_TYPE_MESSAGE, &user_message,
NULL);
GST_DEBUG_OBJECT (player, "Received user message: %" GST_PTR_FORMAT, user_message);
if (gst_message_has_name (user_message, "ClapperPlaylistParsed"))
_on_playlist_parsed_msg (user_message, player);
gst_message_unref (user_message);
}
static inline void
_handle_app_msg (GstMessage *msg, ClapperPlayer *player)
{
@@ -813,6 +910,8 @@ _handle_app_msg (GstMessage *msg, ClapperPlayer *player)
_handle_current_item_change_msg (msg, structure, player);
else if (quark == _STRUCTURE_QUARK (ITEM_SUBURI_CHANGE))
_handle_item_suburi_change_msg (msg, structure, player);
else if (quark == _STRUCTURE_QUARK (USER_MESSAGE))
_handle_user_message_msg (msg, structure, player);
}
static inline void
@@ -832,70 +931,7 @@ _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);
_on_playlist_parsed_msg (msg, player);
} else if (gst_message_has_name (msg, "GstCacheDownloadComplete")) {
ClapperMediaItem *downloaded_item = NULL;
const GstStructure *structure;
@@ -921,6 +957,8 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player)
location = gst_structure_get_string (structure, "location");
signal_id = g_signal_lookup ("download-complete", CLAPPER_TYPE_PLAYER);
/* Set cache location before "download-complete" signal emit,
* so it can also be read directly from item */
GST_INFO_OBJECT (player, "Download of %" GST_PTR_FORMAT
" complete: %s", downloaded_item, location);
clapper_media_item_set_cache_location (downloaded_item, location);

View File

@@ -2245,6 +2245,56 @@ clapper_player_make_pipeline_graph (ClapperPlayer *self, GstDebugGraphDetails de
return gst_debug_bin_to_dot_data (GST_BIN (self->playbin), details);
}
/**
* clapper_player_post_message:
* @player: a #ClapperPlayer
* @msg: (transfer full): a #GstMessage
* @destination: a #ClapperPlayerMessageDestination
*
* Allows sending custom messages to the desired @destination.
*
* This functionality can be used for communication with enhancers implementing
* [iface@Clapper.Reactable] interface. Useful for applications to send custom messages
* to enhacers that can react to them and/or for enhancers development to send events
* from them to the applications. It can also be used for sending specific messages
* from application or enhancers to the player itself.
*
* Messages send to the application can be received by connecting a
* [signal@Clapper.Player::message] signal handler. Inspection of message source
* object can be done to determine who send given message.
*
* Since: 0.10
*/
void
clapper_player_post_message (ClapperPlayer *self, GstMessage *msg,
ClapperPlayerMessageDestination destination)
{
g_return_if_fail (CLAPPER_IS_PLAYER (self));
g_return_if_fail (GST_IS_MESSAGE (msg));
switch (destination) {
case CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER:
clapper_playbin_bus_post_user_message (self->bus, msg);
return; // Message taken
case CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES:
if (self->reactables_manager) {
clapper_reactables_manager_post_message (self->reactables_manager, msg);
return; // Message taken
}
break;
case CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION:
clapper_app_bus_post_message_signal (self->app_bus,
GST_OBJECT_CAST (self), signals[SIGNAL_MESSAGE], msg);
break; // Above function does not take message (unref needed)
default:
g_assert_not_reached ();
break;
}
/* Unref when not taken */
gst_message_unref (msg);
}
static void
clapper_player_thread_start (ClapperThreadedObject *threaded_object)
{
@@ -2962,6 +3012,9 @@ clapper_player_class_init (ClapperPlayerClass *klass)
* be only emitted when progressive download buffering is enabled by
* setting [property@Clapper.Player:download-enabled] property to %TRUE.
*
* Download cache file location can also be read directly from @item
* through its [property@Clapper.MediaItem:cache-location] property.
*
* Since: 0.8
*/
signals[SIGNAL_DOWNLOAD_COMPLETE] = g_signal_new ("download-complete",

View File

@@ -210,4 +210,7 @@ void clapper_player_add_feature (ClapperPlayer *player, ClapperFeature *feature)
CLAPPER_API
gchar * clapper_player_make_pipeline_graph (ClapperPlayer *player, GstDebugGraphDetails details);
CLAPPER_API
void clapper_player_post_message (ClapperPlayer *player, GstMessage *msg, ClapperPlayerMessageDestination destination);
G_END_DECLS

View File

@@ -206,6 +206,17 @@ struct _ClapperReactableInterface
*/
void (* queue_progression_changed) (ClapperReactable *reactable, ClapperQueueProgressionMode mode);
/**
* ClapperReactableInterface::message_received:
* @reactable: a #ClapperReactable
* @msg: a #GstMessage
*
* Custom message from user was received on reactables bus.
*
* Since: 0.10
*/
void (* message_received) (ClapperReactable *reactable, GstMessage *msg);
/*< private >*/
gpointer padding[8];
};

View File

@@ -18,6 +18,8 @@
#pragma once
#include <gst/gst.h>
#include "clapper-enums.h"
#include "clapper-threaded-object.h"
#include "clapper-enhancer-proxy.h"
@@ -36,6 +38,9 @@ void clapper_reactables_manager_initialize (void);
G_GNUC_INTERNAL
ClapperReactablesManager * clapper_reactables_manager_new (void);
G_GNUC_INTERNAL
void clapper_reactables_manager_post_message (ClapperReactablesManager *manager, GstMessage *msg);
G_GNUC_INTERNAL
void clapper_reactables_manager_trigger_configure_take_config (ClapperReactablesManager *manager, ClapperEnhancerProxy *proxy, GstStructure *config);

View File

@@ -78,6 +78,7 @@ enum
{
CLAPPER_REACTABLES_MANAGER_QUARK_CONFIGURE = 0,
CLAPPER_REACTABLES_MANAGER_QUARK_EVENT,
CLAPPER_REACTABLES_MANAGER_QUARK_USER_MESSAGE,
CLAPPER_REACTABLES_MANAGER_QUARK_VALUE,
CLAPPER_REACTABLES_MANAGER_QUARK_EXTRA_VALUE
};
@@ -85,6 +86,7 @@ enum
static ClapperBusQuark _quarks[] = {
{"configure", 0},
{"event", 0},
{"user-message", 0},
{"value", 0},
{"extra-value", 0},
{NULL, 0}
@@ -308,6 +310,23 @@ clapper_reactables_manager_handle_event (ClapperReactablesManager *self, const G
}
}
static inline void
clapper_reactables_manager_handle_user_message (ClapperReactablesManager *self, const GstStructure *structure)
{
const GValue *value = gst_structure_id_get_value (structure, _QUARK (VALUE));
guint i;
for (i = 0; i < self->array->len; ++i) {
ClapperReactableManagerData *data = g_ptr_array_index (self->array, i);
ClapperReactableInterface *reactable_iface = CLAPPER_REACTABLE_GET_IFACE (data->reactable);
if (reactable_iface->message_received) {
reactable_iface->message_received (data->reactable,
GST_MESSAGE_CAST (g_value_get_boxed (value)));
}
}
}
static gboolean
_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSED)
{
@@ -324,6 +343,8 @@ _bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSE
clapper_reactables_manager_handle_event (self, structure);
} else if (quark == _QUARK (CONFIGURE)) {
clapper_reactables_manager_handle_configure (self, structure);
} else if (quark == _QUARK (USER_MESSAGE)) {
clapper_reactables_manager_handle_user_message (self, structure);
} else {
GST_ERROR_OBJECT (self, "Received invalid quark on reactables bus!");
}
@@ -365,6 +386,21 @@ clapper_reactables_manager_new (void)
return reactables_manager;
}
void
clapper_reactables_manager_post_message (ClapperReactablesManager *self, GstMessage *msg)
{
GstStructure *structure = gst_structure_new_id_empty (_QUARK (USER_MESSAGE));
GValue value = G_VALUE_INIT;
g_value_init (&value, GST_TYPE_MESSAGE);
g_value_take_boxed (&value, msg);
gst_structure_id_take_value (structure, _QUARK (VALUE), &value);
gst_bus_post (self->bus, gst_message_new_application (
GST_OBJECT_CAST (self), structure));
}
void
clapper_reactables_manager_trigger_configure_take_config (ClapperReactablesManager *self,
ClapperEnhancerProxy *proxy, GstStructure *config)

View File

@@ -36,6 +36,8 @@
*
* Use [const@Clapper.HAVE_DISCOVERER] macro to check if Clapper API
* was compiled with this feature.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
#include <gst/gst.h>
@@ -369,6 +371,8 @@ clapper_discoverer_unprepare (ClapperFeature *feature)
* Creates a new #ClapperDiscoverer instance.
*
* Returns: (transfer full): a new #ClapperDiscoverer instance.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
ClapperDiscoverer *
clapper_discoverer_new (void)
@@ -385,6 +389,8 @@ clapper_discoverer_new (void)
* @mode: a #ClapperDiscovererDiscoveryMode
*
* Set the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
void
clapper_discoverer_set_discovery_mode (ClapperDiscoverer *self, ClapperDiscovererDiscoveryMode mode)
@@ -409,6 +415,8 @@ clapper_discoverer_set_discovery_mode (ClapperDiscoverer *self, ClapperDiscovere
* Get the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer.
*
* Returns: a currently set #ClapperDiscovererDiscoveryMode.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
ClapperDiscovererDiscoveryMode
clapper_discoverer_get_discovery_mode (ClapperDiscoverer *self)
@@ -498,6 +506,8 @@ clapper_discoverer_class_init (ClapperDiscovererClass *klass)
* ClapperDiscoverer:discovery-mode:
*
* Discoverer discovery mode.
*
* Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead.
*/
param_specs[PROP_DISCOVERY_MODE] = g_param_spec_enum ("discovery-mode",
NULL, NULL, CLAPPER_TYPE_DISCOVERER_DISCOVERY_MODE, DEFAULT_DISCOVERY_MODE,

View File

@@ -33,16 +33,16 @@ G_BEGIN_DECLS
#define CLAPPER_TYPE_DISCOVERER (clapper_discoverer_get_type())
#define CLAPPER_DISCOVERER_CAST(obj) ((ClapperDiscoverer *)(obj))
CLAPPER_API
CLAPPER_DEPRECATED
G_DECLARE_FINAL_TYPE (ClapperDiscoverer, clapper_discoverer, CLAPPER, DISCOVERER, ClapperFeature)
CLAPPER_API
CLAPPER_DEPRECATED
ClapperDiscoverer * clapper_discoverer_new (void);
CLAPPER_API
CLAPPER_DEPRECATED
void clapper_discoverer_set_discovery_mode (ClapperDiscoverer *discoverer, ClapperDiscovererDiscoveryMode mode);
CLAPPER_API
CLAPPER_DEPRECATED
ClapperDiscovererDiscoveryMode clapper_discoverer_get_discovery_mode (ClapperDiscoverer *discoverer);
G_END_DECLS

View File

@@ -60,6 +60,8 @@ typedef struct
GError **error;
} ClapperEnhancerDirectorData;
static GMutex cleanup_lock;
static gpointer
clapper_enhancer_director_extract_in_thread (ClapperEnhancerDirectorData *data)
{
@@ -356,6 +358,11 @@ _cache_cleanup_func (ClapperEnhancerDirector *self)
const gchar *data;
gint64 since_cleanup, epoch_now, epoch_last = 0;
if (!g_mutex_trylock (&cleanup_lock)) {
GST_LOG_OBJECT (self, "Cache cleanup is already running");
return G_SOURCE_REMOVE;
}
date = g_date_time_new_now_utc ();
epoch_now = g_date_time_to_unix (date);
g_date_time_unref (date);
@@ -418,6 +425,7 @@ _cache_cleanup_func (ClapperEnhancerDirector *self)
CLAPPER_TIME_FORMAT " ago", CLAPPER_TIME_ARGS (since_cleanup));
}
g_mutex_unlock (&cleanup_lock);
g_free (filename);
return G_SOURCE_REMOVE;

View File

@@ -30,6 +30,10 @@
#define URI_LIST_MEDIA_TYPE "text/uri-list"
#define DATA_CHUNK_SIZE 4096
#define NTH_REDIRECT_STRUCTURE_NAME "ClapperQueryNthRedirect"
#define NTH_REDIRECT_FIELD "nth-redirect"
#define MAX_REDIRECTS 10
#define GST_CAT_DEFAULT clapper_playlist_demux_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
@@ -309,16 +313,32 @@ _filter_playlistables (ClapperPlaylistDemux *self, GstCaps *caps, ClapperEnhance
static inline gboolean
_handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable *cancellable)
{
ClapperMediaItem *item = g_list_model_get_item (G_LIST_MODEL (playlist), 0);
ClapperMediaItem *item;
GstStructure *structure;
const gchar *uri;
gboolean success;
if (g_cancellable_is_cancelled (cancellable)) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
("Playlist parsing was cancelled"), (NULL));
return FALSE;
}
item = g_list_model_get_item (G_LIST_MODEL (playlist), 0);
if (G_UNLIKELY (item == NULL)) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
("This playlist appears to be empty"), (NULL));
return FALSE;
}
/* Post playlist before setting an URI, so it arrives
* before eventual error (e.g. non-existing file) */
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));
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);
@@ -329,15 +349,51 @@ _handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable
return FALSE;
}
if (!g_cancellable_is_cancelled (cancellable)) {
GstStructure *structure = gst_structure_new ("ClapperPlaylistParsed",
"playlist", G_TYPE_LIST_STORE, playlist, NULL);
return TRUE;
}
gst_element_post_message (GST_ELEMENT_CAST (self),
gst_message_new_element (GST_OBJECT_CAST (self), structure));
static void
_query_parse_nth_redirect (GstQuery *query, guint *nth_redirect)
{
const GstStructure *structure = gst_query_get_structure (query);
*nth_redirect = g_value_get_uint (gst_structure_get_value (structure, NTH_REDIRECT_FIELD));
}
static void
_query_set_nth_redirect (GstQuery *query, guint nth_redirect)
{
GstStructure *structure = gst_query_writable_structure (query);
gst_structure_set (structure, NTH_REDIRECT_FIELD, G_TYPE_UINT, nth_redirect, NULL);
}
static gboolean
clapper_playlist_demux_handle_custom_query (ClapperUriBaseDemux *uri_bd, GstQuery *query)
{
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd);
const GstStructure *structure = gst_query_get_structure (query);
if (gst_structure_has_name (structure, NTH_REDIRECT_STRUCTURE_NAME)) {
GstPad *sink_pad;
GST_LOG_OBJECT (self, "Received custom query: " NTH_REDIRECT_STRUCTURE_NAME);
sink_pad = gst_element_get_static_pad (GST_ELEMENT_CAST (self), "sink");
gst_pad_peer_query (sink_pad, query);
gst_object_unref (sink_pad);
if (G_LIKELY (gst_query_is_writable (query))) {
guint nth_redirect = 0;
_query_parse_nth_redirect (query, &nth_redirect);
_query_set_nth_redirect (query, ++nth_redirect);
} else {
GST_ERROR_OBJECT (self, "Unwritable custom query: " NTH_REDIRECT_STRUCTURE_NAME);
}
return TRUE;
}
return TRUE;
return FALSE;
}
static gboolean
@@ -347,9 +403,11 @@ clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd,
ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd);
GstPad *sink_pad;
GstQuery *query;
GstStructure *query_structure;
GUri *uri = NULL;
GListStore *playlist;
GError *error = NULL;
guint nth_redirect = 0;
gboolean handled;
sink_pad = gst_element_get_static_pad (GST_ELEMENT_CAST (self), "sink");
@@ -367,11 +425,28 @@ clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd,
}
}
gst_query_unref (query);
query_structure = gst_structure_new (NTH_REDIRECT_STRUCTURE_NAME,
NTH_REDIRECT_FIELD, G_TYPE_UINT, 0, NULL);
query = gst_query_new_custom (GST_QUERY_CUSTOM, query_structure);
if (gst_pad_peer_query (sink_pad, query))
_query_parse_nth_redirect (query, &nth_redirect);
GST_DEBUG_OBJECT (self, "Current number of redirects: %u", nth_redirect);
gst_query_unref (query);
gst_object_unref (sink_pad);
if (G_UNLIKELY (uri == NULL)) {
GST_ERROR_OBJECT (self, "Could not query source URI");
GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
("Could not query source URI"), (NULL));
return FALSE;
}
if (G_UNLIKELY (nth_redirect > MAX_REDIRECTS)) {
GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
("Too many nested playlists"), (NULL));
return FALSE;
}
@@ -498,6 +573,7 @@ clapper_playlist_demux_class_init (ClapperPlaylistDemuxClass *klass)
gobject_class->finalize = clapper_playlist_demux_finalize;
clapperuribd_class->handle_caps = clapper_playlist_demux_handle_caps;
clapperuribd_class->handle_custom_query = clapper_playlist_demux_handle_custom_query;
clapperuribd_class->process_buffer = clapper_playlist_demux_process_buffer;
param_specs[PROP_ENHANCER_PROXIES] = g_param_spec_object ("enhancer-proxies",

View File

@@ -41,6 +41,8 @@ struct _ClapperUriBaseDemuxClass
void (* handle_caps) (ClapperUriBaseDemux *uri_bd, GstCaps *caps);
void (* handle_custom_event) (ClapperUriBaseDemux *uri_bd, GstEvent *event);
gboolean (* handle_custom_query) (ClapperUriBaseDemux *uri_bd, GstQuery *query);
};
gboolean clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *uri_bd, const gchar *uri, const gchar *blacklisted_el);

View File

@@ -189,6 +189,20 @@ _make_handler_for_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar
return element;
}
static gboolean
_src_pad_query_func (GstPad *pad, GstObject *parent, GstQuery *query)
{
if (GST_QUERY_TYPE (query) == GST_QUERY_CUSTOM) {
ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (parent);
ClapperUriBaseDemuxClass *uri_bd_class = CLAPPER_URI_BASE_DEMUX_GET_CLASS (self);
if (uri_bd_class->handle_custom_query && uri_bd_class->handle_custom_query (self, query))
return TRUE;
}
return gst_pad_query_default (pad, parent, query);
}
gboolean
clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar *blacklisted_el)
{
@@ -254,6 +268,7 @@ clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *self, const gchar *uri, con
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_query_function (src_ghostpad, (GstPadQueryFunction) _src_pad_query_func);
gst_pad_set_active (src_ghostpad, TRUE);