diff --git a/src/lib/clapper/clapper-app-bus-private.h b/src/lib/clapper/clapper-app-bus-private.h index cef1cfac..6f53dee4 100644 --- a/src/lib/clapper/clapper-app-bus-private.h +++ b/src/lib/clapper/clapper-app-bus-private.h @@ -44,6 +44,8 @@ void clapper_app_bus_post_refresh_streams (ClapperAppBus *app_bus, GstObject *sr void clapper_app_bus_post_refresh_timeline (ClapperAppBus *app_bus, GstObject *src); +void clapper_app_bus_post_insert_playlist (ClapperAppBus *app_bus, GstObject *src, GstObject *playlist_item, GObject *playlist); + void clapper_app_bus_post_simple_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id); void clapper_app_bus_post_object_desc_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, GstObject *object, const gchar *desc); diff --git a/src/lib/clapper/clapper-app-bus.c b/src/lib/clapper/clapper-app-bus.c index eb55d05f..b70e4fd1 100644 --- a/src/lib/clapper/clapper-app-bus.c +++ b/src/lib/clapper/clapper-app-bus.c @@ -21,6 +21,7 @@ #include "clapper-bus-private.h" #include "clapper-app-bus-private.h" #include "clapper-player-private.h" +#include "clapper-queue-private.h" #include "clapper-media-item-private.h" #include "clapper-timeline-private.h" @@ -41,6 +42,7 @@ enum CLAPPER_APP_BUS_STRUCTURE_PROP_NOTIFY, CLAPPER_APP_BUS_STRUCTURE_REFRESH_STREAMS, CLAPPER_APP_BUS_STRUCTURE_REFRESH_TIMELINE, + CLAPPER_APP_BUS_STRUCTURE_INSERT_PLAYLIST, CLAPPER_APP_BUS_STRUCTURE_SIMPLE_SIGNAL, CLAPPER_APP_BUS_STRUCTURE_OBJECT_DESC_SIGNAL, CLAPPER_APP_BUS_STRUCTURE_DESC_WITH_DETAILS_SIGNAL, @@ -52,6 +54,7 @@ static ClapperBusQuark _structure_quarks[] = { {"prop-notify", 0}, {"refresh-streams", 0}, {"refresh-timeline", 0}, + {"insert-playlist", 0}, {"simple-signal", 0}, {"object-desc-signal", 0}, {"desc-with-details-signal", 0}, @@ -65,6 +68,7 @@ enum CLAPPER_APP_BUS_FIELD_PSPEC, CLAPPER_APP_BUS_FIELD_SIGNAL_ID, CLAPPER_APP_BUS_FIELD_OBJECT, + CLAPPER_APP_BUS_FIELD_OTHER_OBJECT, CLAPPER_APP_BUS_FIELD_DESC, CLAPPER_APP_BUS_FIELD_DETAILS, CLAPPER_APP_BUS_FIELD_ERROR, @@ -76,6 +80,7 @@ static ClapperBusQuark _field_quarks[] = { {"pspec", 0}, {"signal-id", 0}, {"object", 0}, + {"other-object", 0}, {"desc", 0}, {"details", 0}, {"error", 0}, @@ -160,6 +165,36 @@ _handle_refresh_timeline_msg (GstMessage *msg, const GstStructure *structure) clapper_timeline_refresh (timeline); } +void +clapper_app_bus_post_insert_playlist (ClapperAppBus *self, GstObject *src, + GstObject *playlist_item, GObject *playlist) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (INSERT_PLAYLIST), + _FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, playlist_item, + _FIELD_QUARK (OTHER_OBJECT), G_TYPE_OBJECT, playlist, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_insert_playlist_msg (GstMessage *msg, const GstStructure *structure) +{ + ClapperPlayer *player = CLAPPER_PLAYER_CAST (GST_MESSAGE_SRC (msg)); + ClapperQueue *queue = clapper_player_get_queue (player); + GstObject *playlist_item; + GObject *playlist; + + gst_structure_id_get (structure, + _FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, &playlist_item, + _FIELD_QUARK (OTHER_OBJECT), G_TYPE_OBJECT, &playlist, + NULL); + clapper_queue_handle_playlist (queue, + CLAPPER_MEDIA_ITEM (playlist_item), G_LIST_STORE (playlist)); + + gst_object_unref (playlist_item); + g_object_unref (playlist); +} + void clapper_app_bus_post_simple_signal (ClapperAppBus *self, GstObject *src, guint signal_id) { @@ -285,6 +320,8 @@ clapper_app_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G _handle_refresh_streams_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (REFRESH_TIMELINE)) _handle_refresh_timeline_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (INSERT_PLAYLIST)) + _handle_insert_playlist_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (SIMPLE_SIGNAL)) _handle_simple_signal_msg (msg, structure); else if (quark == _STRUCTURE_QUARK (OBJECT_DESC_SIGNAL)) diff --git a/src/lib/clapper/clapper-media-item-private.h b/src/lib/clapper/clapper-media-item-private.h index 3ae8470b..d1666355 100644 --- a/src/lib/clapper/clapper-media-item-private.h +++ b/src/lib/clapper/clapper-media-item-private.h @@ -33,6 +33,9 @@ void clapper_media_item_update_from_tag_list (ClapperMediaItem *item, const GstT G_GNUC_INTERNAL void clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDiscovererInfo *info); +G_GNUC_INTERNAL +gboolean clapper_media_item_update_from_item (ClapperMediaItem *item, ClapperMediaItem *other_item, ClapperPlayer *player); + G_GNUC_INTERNAL gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus); diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index f508e3d1..ea89529b 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -51,6 +51,10 @@ struct _ClapperMediaItem gchar *container_format; gdouble duration; + /* Whether using title from URI */ + gboolean title_is_parsed; + + GSList *redirects; gchar *cache_uri; /* For shuffle */ @@ -198,6 +202,12 @@ clapper_media_item_get_id (ClapperMediaItem *self) return self->id; } +/* FIXME: 1.0: + * Consider change to be transfer-full and just return latest data from redirects + * list (alternatively expose redirect URI). This should make it possible to work + * with enhancers that would benefit from knowledge about URI changes + * (e.g "Recall" could read actual media instead of playlist file). + */ /** * clapper_media_item_get_uri: * @item: a #ClapperMediaItem @@ -295,11 +305,11 @@ clapper_media_item_get_title (ClapperMediaItem *self) static inline gboolean _refresh_tag_prop_unlocked (ClapperMediaItem *self, const gchar *tag, - gboolean from_user, gchar **tag_ptr) + gboolean allow_overwrite, gchar **tag_ptr) { const gchar *string; - if ((*tag_ptr && from_user) // if already set, user cannot modify it + if ((*tag_ptr && !allow_overwrite) || !gst_tag_list_peek_string_index (self->tags, tag, 0, &string) // guarantees non-empty string || (g_strcmp0 (*tag_ptr, string) == 0)) return FALSE; @@ -488,11 +498,12 @@ clapper_media_item_insert_tags_internal (ClapperMediaItem *self, const GstTagLis *flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TAGS; if ((title_changed = _refresh_tag_prop_unlocked (self, GST_TAG_TITLE, - from_user, &self->title))) { + (!from_user || self->title_is_parsed), &self->title))) { + self->title_is_parsed = FALSE; *flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; } cont_changed = _refresh_tag_prop_unlocked (self, GST_TAG_CONTAINER_FORMAT, - from_user, &self->container_format); + !from_user, &self->container_format); } GST_OBJECT_UNLOCK (self); @@ -668,6 +679,61 @@ clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDisco gst_object_unref (player); } +/* XXX: Must be set from player thread */ +static inline gboolean +clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri) +{ + /* Check if we did not already redirect into that URI (prevent endless loop) */ + if (!redirect_uri || g_slist_find_custom (self->redirects, redirect_uri, (GCompareFunc) strcmp)) + return FALSE; + + self->redirects = g_slist_prepend (self->redirects, g_strdup (redirect_uri)); + GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\"", (gchar *) self->redirects->data); + + return TRUE; +} + +gboolean +clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *other_item, + ClapperPlayer *player) +{ + gboolean title_changed = FALSE; + + if (!clapper_media_item_set_redirect_uri (self, clapper_media_item_get_uri (other_item))) + return FALSE; + + GST_OBJECT_LOCK (other_item); + + if (other_item->tags) + clapper_media_item_update_from_tag_list (self, other_item->tags, player); + + /* Since its redirect now, we have to update title to describe new file instead of + * being a playlist title. If other item had parsed title, it also means that tags + * did not contain it, thus we have to manually update it and notify. */ + if (other_item->title_is_parsed) { + GST_OBJECT_LOCK (self); + title_changed = g_set_str (&self->title, other_item->title); + self->title_is_parsed = TRUE; + GST_OBJECT_UNLOCK (self); + } + + GST_OBJECT_UNLOCK (other_item); + + if (title_changed) { + ClapperReactableItemUpdatedFlags flags = CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; + ClapperFeaturesManager *features_manager; + + clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]); + + if (player->reactables_manager) + clapper_reactables_manager_trigger_item_updated (player->reactables_manager, self, flags); + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, self); + } + + return TRUE; +} + /* XXX: Must be set from player thread or upon construction */ void clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location) @@ -682,7 +748,7 @@ clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *loca } /* XXX: Can only be read from player thread. - * Returns cache URI if available, item URI otherwise. */ + * Returns cache URI if available, otherwise redirect or item URI. */ inline const gchar * clapper_media_item_get_playback_uri (ClapperMediaItem *self) { @@ -702,6 +768,9 @@ clapper_media_item_get_playback_uri (ClapperMediaItem *self) clapper_media_item_set_cache_location (self, NULL); } + if (self->redirects) + return self->redirects->data; + return self->uri; } @@ -745,6 +814,7 @@ clapper_media_item_constructed (GObject *object) self->uri = g_strdup ("file://"); self->title = clapper_utils_title_from_uri (self->uri); + self->title_is_parsed = (self->title != NULL); G_OBJECT_CLASS (parent_class)->constructed (object); } @@ -765,6 +835,7 @@ clapper_media_item_finalize (GObject *object) gst_object_unparent (GST_OBJECT_CAST (self->timeline)); gst_object_unref (self->timeline); + g_slist_free_full (self->redirects, g_free); g_free (self->cache_uri); G_OBJECT_CLASS (parent_class)->finalize (object); @@ -892,7 +963,8 @@ clapper_media_item_class_init (ClapperMediaItemClass *klass) G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /* FIXME: 1.0: Consider rename to e.g. "(menu/display)-title" - * and also make it non-nullable (return URI as final fallback) */ + * and also make it non-nullable (return URI as final fallback). + * NOTE: It would probably need to work with redirect URI */ /** * ClapperMediaItem:title: * diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index ed022c08..4cb6a1c5 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -831,6 +831,71 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player) g_free (name); g_free (details); + } else if (gst_message_has_name (msg, "ClapperPlaylistParsed")) { + ClapperMediaItem *playlist_item = NULL; + GListStore *playlist = NULL; + const GstStructure *structure = gst_message_get_structure (msg); + guint n_items; + + /* If message contains item, use that. + * Otherwise assume pending item was parsed. */ + if (gst_structure_has_field (structure, "item")) { + gst_structure_get (structure, + "item", CLAPPER_TYPE_MEDIA_ITEM, &playlist_item, NULL); + } else { + GST_OBJECT_LOCK (player); + + /* Playlist is always parsed before playback starts */ + if (player->pending_item) + playlist_item = gst_object_ref (player->pending_item); + + GST_OBJECT_UNLOCK (player); + } + + if (G_UNLIKELY (playlist_item == NULL)) { + GST_WARNING_OBJECT (player, "Playlist parsed without media item set"); + return; + } + + GST_INFO_OBJECT (player, "Received parsed playlist of %" GST_PTR_FORMAT + "(%s)", playlist_item, clapper_media_item_get_uri (playlist_item)); + + gst_structure_get (structure, + "playlist", G_TYPE_LIST_STORE, &playlist, NULL); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (playlist)); + + if (G_LIKELY (n_items > 0)) { + ClapperMediaItem *active_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + gboolean updated; + + /* Update redirect URI (must be done from player thread) */ + updated = clapper_media_item_update_from_item (playlist_item, active_item, player); + gst_object_unref (active_item); + + if (!updated) { + GstMessage *msg; + GError *error; + + error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, + "Detected infinite redirection in playlist"); + msg = gst_message_new_error (GST_OBJECT (player), error, NULL); + + _handle_error_msg (msg, player); + + g_error_free (error); + gst_message_unref (msg); + } else if (n_items > 1) { + /* Forward to append remaining items (must be done from main thread) */ + clapper_app_bus_post_insert_playlist (player->app_bus, + GST_OBJECT_CAST (player), + GST_OBJECT_CAST (playlist_item), + G_OBJECT (playlist)); + } + } + + gst_object_unref (playlist_item); + g_object_unref (playlist); } else if (gst_message_has_name (msg, "GstCacheDownloadComplete")) { ClapperMediaItem *downloaded_item = NULL; const GstStructure *structure; diff --git a/src/lib/clapper/clapper-queue-private.h b/src/lib/clapper/clapper-queue-private.h index 87f99452..daa000c4 100644 --- a/src/lib/clapper/clapper-queue-private.h +++ b/src/lib/clapper/clapper-queue-private.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include "clapper-queue.h" #include "clapper-media-item.h" @@ -31,6 +32,8 @@ ClapperQueue * clapper_queue_new (void); void clapper_queue_handle_played_item_changed (ClapperQueue *queue, ClapperMediaItem *played_item, ClapperAppBus *app_bus); +void clapper_queue_handle_playlist (ClapperQueue *queue, ClapperMediaItem *playlist_item, GListStore *playlist); + void clapper_queue_handle_about_to_finish (ClapperQueue *queue, ClapperPlayer *player); gboolean clapper_queue_handle_eos (ClapperQueue *queue, ClapperPlayer *player); diff --git a/src/lib/clapper/clapper-queue.c b/src/lib/clapper/clapper-queue.c index 7f1ecd64..faf3b7b4 100644 --- a/src/lib/clapper/clapper-queue.c +++ b/src/lib/clapper/clapper-queue.c @@ -337,6 +337,42 @@ _get_next_item_unlocked (ClapperQueue *self, ClapperQueueProgressionMode mode) return next_item; } +static void +_take_item_unlocked (ClapperQueue *self, ClapperMediaItem *item, gint index) +{ + guint prev_length = self->items->len; + + g_ptr_array_insert (self->items, index, item); + gst_object_set_parent (GST_OBJECT_CAST (item), GST_OBJECT_CAST (self)); + + /* In append we inserted at array length */ + if (index < 0) + index = prev_length; + + _announce_model_update (self, index, 0, 1, item); + + /* If has selection and inserting before it */ + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && (guint) index <= self->current_index) { + self->current_index++; + _announce_current_index_change (self); + } else if (prev_length == 0 && _replace_current_item_unlocked (self, item, 0)) { + /* If queue was empty, auto select first item and announce it */ + _announce_current_item_and_index_change (self); + } else if (self->current_index == prev_length - 1 + && clapper_queue_get_progression_mode (self) == CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + gboolean after_eos = (gboolean) g_atomic_int_get (&player->eos); + + /* In consecutive progression automatically select next item + * if we were after EOS of last queue item */ + if (after_eos && _replace_current_item_unlocked (self, item, index)) + _announce_current_item_and_index_change (self); + + gst_object_unref (player); + } +} + /* * For gapless we need to manually replace current item in queue when it starts * playing and emit notify about change, this function will do that if necessary @@ -366,6 +402,31 @@ clapper_queue_handle_played_item_changed (ClapperQueue *self, ClapperMediaItem * } } +/* Must be called from main thread */ +void +clapper_queue_handle_playlist (ClapperQueue *self, ClapperMediaItem *playlist_item, + GListStore *playlist) +{ + GListModel *playlist_model = G_LIST_MODEL (playlist); + guint i, index, n_items = g_list_model_get_n_items (playlist_model); + + CLAPPER_QUEUE_REC_LOCK (self); + + /* If playlist item is still in the queue, insert + * remaining items after it, otherwise append */ + if (G_LIKELY (g_ptr_array_find (self->items, playlist_item, &index))) + index++; + else + index = self->items->len; + + for (i = 1; i < n_items; ++i) { + ClapperMediaItem *item = g_list_model_get_item (playlist_model, i); + _take_item_unlocked (self, item, index++); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + void clapper_queue_handle_about_to_finish (ClapperQueue *self, ClapperPlayer *player) { @@ -481,39 +542,8 @@ clapper_queue_insert_item (ClapperQueue *self, ClapperMediaItem *item, gint inde CLAPPER_QUEUE_REC_LOCK (self); - if (!g_ptr_array_find (self->items, item, NULL)) { - guint prev_length = self->items->len; - - g_ptr_array_insert (self->items, index, gst_object_ref (item)); - gst_object_set_parent (GST_OBJECT_CAST (item), GST_OBJECT_CAST (self)); - - /* In append we inserted at array length */ - if (index < 0) - index = prev_length; - - _announce_model_update (self, index, 0, 1, item); - - /* If has selection and inserting before it */ - if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION - && (guint) index <= self->current_index) { - self->current_index++; - _announce_current_index_change (self); - } else if (prev_length == 0 && _replace_current_item_unlocked (self, item, 0)) { - /* If queue was empty, auto select first item and announce it */ - _announce_current_item_and_index_change (self); - } else if (self->current_index == prev_length - 1 - && clapper_queue_get_progression_mode (self) == CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE) { - ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); - gboolean after_eos = (gboolean) g_atomic_int_get (&player->eos); - - /* In consecutive progression automatically select next item - * if we were after EOS of last queue item */ - if (after_eos && _replace_current_item_unlocked (self, item, index)) - _announce_current_item_and_index_change (self); - - gst_object_unref (player); - } - } + if (!g_ptr_array_find (self->items, item, NULL)) + _take_item_unlocked (self, gst_object_ref (item), index); CLAPPER_QUEUE_REC_UNLOCK (self); }