Files
clapper/src/lib/clapper/clapper-queue.c
Benjamin A. Beasley b5cc171803 Update LGPL-2.1-or-later license notices for remote-only FSF
The Free Software Foundation is now remote-only, and no longer has a
street address. Updated license notice text is from
https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html#SEC4.
2025-06-14 11:35:02 -04:00

1317 lines
37 KiB
C

/* 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/>.
*/
/**
* ClapperQueue:
*
* A queue of media to be played.
*/
#include <gio/gio.h>
#include "clapper-queue-private.h"
#include "clapper-media-item-private.h"
#include "clapper-player-private.h"
#include "clapper-playbin-bus-private.h"
#include "clapper-reactables-manager-private.h"
#include "clapper-features-manager-private.h"
#define CLAPPER_QUEUE_GET_REC_LOCK(obj) (&CLAPPER_QUEUE_CAST(obj)->rec_lock)
#define CLAPPER_QUEUE_REC_LOCK(obj) g_rec_mutex_lock (CLAPPER_QUEUE_GET_REC_LOCK(obj))
#define CLAPPER_QUEUE_REC_UNLOCK(obj) g_rec_mutex_unlock (CLAPPER_QUEUE_GET_REC_LOCK(obj))
#define DEFAULT_PROGRESSION_MODE CLAPPER_QUEUE_PROGRESSION_NONE
#define DEFAULT_GAPLESS FALSE
#define DEFAULT_INSTANT FALSE
#define GST_CAT_DEFAULT clapper_queue_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
struct _ClapperQueue
{
GstObject parent;
GRecMutex rec_lock;
GPtrArray *items;
ClapperMediaItem *current_item;
guint current_index;
ClapperQueueProgressionMode progression_mode;
gboolean gapless;
gboolean instant;
/* Avoid scenario when "gapless" prop is changed
* between "about-to-finish" and "EOS" */
gboolean handled_gapless;
};
enum
{
PROP_0,
PROP_CURRENT_ITEM,
PROP_CURRENT_INDEX,
PROP_N_ITEMS,
PROP_PROGRESSION_MODE,
PROP_GAPLESS,
PROP_INSTANT,
PROP_LAST
};
static GType
clapper_queue_list_model_get_item_type (GListModel *model)
{
return CLAPPER_TYPE_MEDIA_ITEM;
}
static guint
clapper_queue_list_model_get_n_items (GListModel *model)
{
ClapperQueue *self = CLAPPER_QUEUE_CAST (model);
guint n_items;
CLAPPER_QUEUE_REC_LOCK (self);
n_items = self->items->len;
CLAPPER_QUEUE_REC_UNLOCK (self);
return n_items;
}
static gpointer
clapper_queue_list_model_get_item (GListModel *model, guint index)
{
ClapperQueue *self = CLAPPER_QUEUE_CAST (model);
ClapperMediaItem *item = NULL;
CLAPPER_QUEUE_REC_LOCK (self);
if (G_LIKELY (index < self->items->len)) {
GST_LOG_OBJECT (self, "Reading queue item: %u", index);
item = g_object_ref (g_ptr_array_index (self->items, index));
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return item;
}
static void
clapper_queue_list_model_iface_init (GListModelInterface *iface)
{
iface->get_item_type = clapper_queue_list_model_get_item_type;
iface->get_n_items = clapper_queue_list_model_get_n_items;
iface->get_item = clapper_queue_list_model_get_item;
}
#define parent_class clapper_queue_parent_class
G_DEFINE_TYPE_WITH_CODE (ClapperQueue, clapper_queue, GST_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, clapper_queue_list_model_iface_init));
static GParamSpec *param_specs[PROP_LAST] = { NULL, };
static void
_announce_model_update (ClapperQueue *self, guint index, guint removed, guint added,
ClapperMediaItem *changed_item)
{
GST_DEBUG_OBJECT (self, "Announcing model update, index: %u, removed: %u, added: %u",
index, removed, added);
/* We handle reposition separately */
if (removed != added) {
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
if (player) {
gboolean have_features = clapper_player_get_have_features (player);
if (added == 1) { // addition
if (player->reactables_manager)
clapper_reactables_manager_trigger_queue_item_added (player->reactables_manager, changed_item, index);
if (have_features)
clapper_features_manager_trigger_queue_item_added (player->features_manager, changed_item, index);
} else if (removed == 1) { // removal
if (player->reactables_manager)
clapper_reactables_manager_trigger_queue_item_removed (player->reactables_manager, changed_item, index);
if (have_features)
clapper_features_manager_trigger_queue_item_removed (player->features_manager, changed_item, index);
} else if (removed > 1 && added == 0) { // queue cleared
if (player->reactables_manager)
clapper_reactables_manager_trigger_queue_cleared (player->reactables_manager);
if (have_features)
clapper_features_manager_trigger_queue_cleared (player->features_manager);
} else {
g_assert_not_reached ();
}
}
gst_clear_object (&player);
}
g_list_model_items_changed (G_LIST_MODEL (self), index, removed, added);
if (removed != added)
g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_ITEMS]);
}
static void
_announce_reposition (ClapperQueue *self, guint before, guint after)
{
ClapperPlayer *player;
GST_DEBUG_OBJECT (self, "Announcing item reposition: %u -> %u", before, after);
if ((player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) {
if (player->reactables_manager)
clapper_reactables_manager_trigger_queue_item_repositioned (player->reactables_manager, before, after);
if (clapper_player_get_have_features (player))
clapper_features_manager_trigger_queue_item_repositioned (player->features_manager, before, after);
gst_object_unref (player);
}
}
/*
* Notify about current index change. This is needed only if some items
* are added/removed before current selection, otherwise if selection
* also changes use _announce_current_item_and_index_change() instead.
*/
static void
_announce_current_index_change (ClapperQueue *self)
{
gboolean is_main_thread = g_main_context_is_owner (g_main_context_default ());
GST_DEBUG_OBJECT (self, "Announcing current index change from %smain thread, now: %u",
(is_main_thread) ? "" : "non-", self->current_index);
if (is_main_thread) {
g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_INDEX]);
} else {
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
if (G_LIKELY (player != NULL)) {
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]);
gst_object_unref (player);
}
}
}
/*
* Notify about both current item and its index changes.
* Needs to be called while holding CLAPPER_QUEUE_REC_LOCK.
*/
static void
_announce_current_item_and_index_change (ClapperQueue *self)
{
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
gboolean instant, is_main_thread;
if (G_UNLIKELY (player == NULL))
return;
is_main_thread = g_main_context_is_owner (g_main_context_default ());
GST_DEBUG_OBJECT (self, "Announcing current item change from %smain thread,"
" now: %" GST_PTR_FORMAT " (index: %u)",
(is_main_thread) ? "" : "non-", self->current_item, self->current_index);
GST_OBJECT_LOCK (self);
instant = self->instant;
GST_OBJECT_UNLOCK (self);
clapper_playbin_bus_post_current_item_change (player->bus, self->current_item,
(instant) ? CLAPPER_QUEUE_ITEM_CHANGE_INSTANT : CLAPPER_QUEUE_ITEM_CHANGE_NORMAL);
if (is_main_thread) {
g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_ITEM]);
g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_INDEX]);
} else {
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_ITEM]);
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]);
}
gst_object_unref (player);
}
static inline gboolean
_replace_current_item_unlocked (ClapperQueue *self, ClapperMediaItem *item, guint index)
{
if (gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (item))) {
self->current_index = index;
if (self->current_item)
clapper_media_item_set_used (self->current_item, TRUE);
GST_TRACE_OBJECT (self, "Current item replaced, now: %" GST_PTR_FORMAT, self->current_item);
return TRUE;
}
return FALSE;
}
static void
_reset_shuffle_unlocked (ClapperQueue *self)
{
guint i;
for (i = 0; i < self->items->len; ++i) {
ClapperMediaItem *item = g_ptr_array_index (self->items, i);
clapper_media_item_set_used (item, FALSE);
}
}
static ClapperMediaItem *
_get_next_item_unlocked (ClapperQueue *self, ClapperQueueProgressionMode mode)
{
ClapperMediaItem *next_item = NULL;
GST_DEBUG_OBJECT (self, "Handling progression mode: %u", mode);
if (self->current_index == CLAPPER_QUEUE_INVALID_POSITION) {
GST_DEBUG_OBJECT (self, "No current item, can not advance");
return NULL;
}
switch (mode) {
case CLAPPER_QUEUE_PROGRESSION_NONE:
break;
case CLAPPER_QUEUE_PROGRESSION_CAROUSEL:
next_item = g_ptr_array_index (self->items, 0);
G_GNUC_FALLTHROUGH;
case CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE:
if (self->current_index + 1 < self->items->len)
next_item = g_ptr_array_index (self->items, self->current_index + 1);
break;
case CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM:
next_item = self->current_item;
break;
case CLAPPER_QUEUE_PROGRESSION_SHUFFLE:{
GList *unused = NULL;
GRand *rand = g_rand_new ();
guint i;
for (i = 0; i < self->items->len; ++i) {
ClapperMediaItem *item = g_ptr_array_index (self->items, i);
if (!clapper_media_item_get_used (item))
unused = g_list_append (unused, item);
}
if (unused) {
next_item = g_list_nth_data (unused,
g_rand_int_range (rand, 0, g_list_length (unused)));
g_list_free (unused);
} else {
_reset_shuffle_unlocked (self);
next_item = g_ptr_array_index (self->items,
g_rand_int_range (rand, 0, self->items->len));
}
g_rand_free (rand);
break;
}
default:
g_assert_not_reached ();
break;
}
if (next_item)
gst_object_ref (next_item);
return next_item;
}
/*
* 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
*/
void
clapper_queue_handle_played_item_changed (ClapperQueue *self, ClapperMediaItem *played_item,
ClapperAppBus *app_bus)
{
guint index = 0;
gboolean changed = FALSE;
CLAPPER_QUEUE_REC_LOCK (self);
/* Item is often the same here (when selected from queue),
* so compare pointers first to avoid iterating queue */
if (played_item != self->current_item
&& g_ptr_array_find (self->items, played_item, &index))
changed = _replace_current_item_unlocked (self, played_item, index);
CLAPPER_QUEUE_REC_UNLOCK (self);
if (changed) {
clapper_app_bus_post_prop_notify (app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_ITEM]);
clapper_app_bus_post_prop_notify (app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]);
}
}
void
clapper_queue_handle_about_to_finish (ClapperQueue *self, ClapperPlayer *player)
{
ClapperMediaItem *next_item;
ClapperQueueProgressionMode progression_mode;
GST_INFO_OBJECT (self, "Handling \"about-to-finish\"");
GST_OBJECT_LOCK (self);
if (!(self->handled_gapless = self->gapless)) {
GST_OBJECT_UNLOCK (self);
return;
}
progression_mode = self->progression_mode;
GST_OBJECT_UNLOCK (self);
CLAPPER_QUEUE_REC_LOCK (self);
next_item = _get_next_item_unlocked (self, progression_mode);
CLAPPER_QUEUE_REC_UNLOCK (self);
if (next_item) {
clapper_player_set_pending_item (player, next_item, CLAPPER_QUEUE_ITEM_CHANGE_GAPLESS);
gst_object_unref (next_item);
}
}
gboolean
clapper_queue_handle_eos (ClapperQueue *self, ClapperPlayer *player)
{
ClapperMediaItem *next_item = NULL;
ClapperQueueProgressionMode progression_mode;
gboolean handled_eos = FALSE;
/* On gapless "about-to-finish" selects next item instead and
* we can reach EOS only if there was either nothing to select or
* some playback error ocurred */
GST_INFO_OBJECT (self, "Handling EOS");
GST_OBJECT_LOCK (self);
if (self->handled_gapless) {
self->handled_gapless = FALSE; // reset
GST_OBJECT_UNLOCK (self);
return FALSE;
}
progression_mode = self->progression_mode;
GST_OBJECT_UNLOCK (self);
CLAPPER_QUEUE_REC_LOCK (self);
if ((next_item = _get_next_item_unlocked (self, progression_mode))) {
if (next_item == self->current_item)
clapper_player_seek (player, 0);
else
clapper_queue_select_item (self, next_item);
handled_eos = TRUE;
gst_object_unref (next_item);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return handled_eos;
}
/*
* clapper_queue_new:
*
* Returns: (transfer full): a new #ClapperQueue instance
*/
ClapperQueue *
clapper_queue_new (void)
{
ClapperQueue *queue;
queue = g_object_new (CLAPPER_TYPE_QUEUE, NULL);
gst_object_ref_sink (queue);
return queue;
}
/**
* clapper_queue_add_item:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem
*
* Add another #ClapperMediaItem to the end of queue.
*
* If item is already in queue, this function will do nothing,
* so it is safe to call multiple times if unsure.
*/
void
clapper_queue_add_item (ClapperQueue *self, ClapperMediaItem *item)
{
clapper_queue_insert_item (self, item, -1);
}
/**
* clapper_queue_insert_item:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem
* @index: the index to place @item in queue, -1 to append
*
* Insert another #ClapperMediaItem at @index position to the queue.
*
* If item is already in queue, this function will do nothing,
* so it is safe to call multiple times if unsure.
*/
void
clapper_queue_insert_item (ClapperQueue *self, ClapperMediaItem *item, gint index)
{
g_return_if_fail (CLAPPER_IS_QUEUE (self));
g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item));
g_return_if_fail (index >= -1);
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);
}
}
CLAPPER_QUEUE_REC_UNLOCK (self);
}
/**
* clapper_queue_reposition_item:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem
* @index: the index to place @item in queue, -1 to place at the end
*
* Change position of one #ClapperMediaItem within the queue.
*
* Note that the @index is the new position you expect item to be
* after whole reposition operation is finished.
*
* If item is not in the queue, this function will do nothing.
*/
void
clapper_queue_reposition_item (ClapperQueue *self, ClapperMediaItem *item, gint index)
{
guint index_old = 0;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item));
g_return_if_fail (index >= -1);
CLAPPER_QUEUE_REC_LOCK (self);
if (g_ptr_array_find (self->items, item, &index_old)) {
ClapperMediaItem *removed_item;
guint index_new, start_index, end_index, n_changed;
index_new = (index < 0)
? self->items->len - 1
: (guint) index;
GST_DEBUG_OBJECT (self, "Reposition item %u -> %u, is_current: %s",
index_old, index_new, (item == self->current_item) ? "yes" : "no");
removed_item = g_ptr_array_steal_index (self->items, index_old);
g_ptr_array_insert (self->items, index_new, removed_item);
_announce_reposition (self, index_old, index_new);
if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION) {
guint before = self->current_index;
if (index_old > self->current_index && index_new <= self->current_index)
self->current_index++; // Moved before current item
else if (index_old < self->current_index && index_new >= self->current_index)
self->current_index--; // Moved after current item
else if (index_old == self->current_index)
self->current_index = index_new; // Moved current item
if (self->current_index != before)
_announce_current_index_change (self);
}
start_index = MIN (index_old, index_new);
end_index = MAX (index_old, index_new);
n_changed = end_index - start_index + 1;
_announce_model_update (self, start_index, n_changed, n_changed, item);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
}
/**
* clapper_queue_remove_item:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem
*
* Removes #ClapperMediaItem from the queue.
*
* If item either was never in the queue or was removed from
* it earlier, this function will do nothing, so it is safe
* to call multiple times if unsure.
*/
void
clapper_queue_remove_item (ClapperQueue *self, ClapperMediaItem *item)
{
guint index = 0;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item));
CLAPPER_QUEUE_REC_LOCK (self);
if (g_ptr_array_find (self->items, item, &index))
clapper_queue_remove_index (self, index);
CLAPPER_QUEUE_REC_UNLOCK (self);
}
/**
* clapper_queue_remove_index:
* @queue: a #ClapperQueue
* @index: an item index
*
* Removes #ClapperMediaItem at @index from the queue.
*/
void
clapper_queue_remove_index (ClapperQueue *self, guint index)
{
ClapperMediaItem *item = clapper_queue_steal_index (self, index);
gst_clear_object (&item);
}
/**
* clapper_queue_steal_index:
* @queue: a #ClapperQueue
* @index: an item index
*
* Removes #ClapperMediaItem at @index from the queue.
*
* Returns: (transfer full) (nullable): The removed #ClapperMediaItem at @index.
*/
ClapperMediaItem *
clapper_queue_steal_index (ClapperQueue *self, guint index)
{
ClapperMediaItem *removed_item = NULL;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL);
g_return_val_if_fail (index != CLAPPER_QUEUE_INVALID_POSITION, NULL);
CLAPPER_QUEUE_REC_LOCK (self);
if (index < self->items->len) {
if (index == self->current_index
&& _replace_current_item_unlocked (self, NULL, CLAPPER_QUEUE_INVALID_POSITION)) {
_announce_current_item_and_index_change (self);
} else if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION
&& index < self->current_index) {
/* If has selection and removed before it */
self->current_index--;
_announce_current_index_change (self);
}
removed_item = g_ptr_array_steal_index (self->items, index);
gst_object_unparent (GST_OBJECT_CAST (removed_item));
_announce_model_update (self, index, 1, 0, removed_item);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return removed_item;
}
/**
* clapper_queue_clear:
* @queue: a #ClapperQueue
*
* Removes all media items from the queue.
*
* If queue is empty, this function will do nothing,
* so it is safe to call multiple times if unsure.
*/
void
clapper_queue_clear (ClapperQueue *self)
{
guint n_items;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
CLAPPER_QUEUE_REC_LOCK (self);
n_items = self->items->len;
if (n_items > 0) {
if (_replace_current_item_unlocked (self, NULL, CLAPPER_QUEUE_INVALID_POSITION))
_announce_current_item_and_index_change (self);
g_ptr_array_remove_range (self->items, 0, n_items);
_announce_model_update (self, 0, n_items, 0, NULL);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
}
/**
* clapper_queue_select_item:
* @queue: a #ClapperQueue
* @item: (nullable): a #ClapperMediaItem or %NULL to unselect
*
* Selects #ClapperMediaItem from @queue as current one or
* unselects currently selected item when @item is %NULL.
*
* Returns: %TRUE if item could be selected/unselected,
* %FALSE if it was not in the queue.
*/
gboolean
clapper_queue_select_item (ClapperQueue *self, ClapperMediaItem *item)
{
gboolean success = FALSE;
guint index = 0;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
g_return_val_if_fail (item == NULL || CLAPPER_IS_MEDIA_ITEM (item), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
if (!item)
success = clapper_queue_select_index (self, CLAPPER_QUEUE_INVALID_POSITION);
else if (g_ptr_array_find (self->items, item, &index))
success = clapper_queue_select_index (self, index);
CLAPPER_QUEUE_REC_UNLOCK (self);
return success;
}
/**
* clapper_queue_select_index:
* @queue: a #ClapperQueue
* @index: an item index or [const@Clapper.QUEUE_INVALID_POSITION] to unselect
*
* Selects #ClapperMediaItem at @index from @queue as current one or
* unselects currently selected index when @index is [const@Clapper.QUEUE_INVALID_POSITION].
*
* Returns: %TRUE if item at @index could be selected/unselected,
* %FALSE if index was out of queue range.
*/
gboolean
clapper_queue_select_index (ClapperQueue *self, guint index)
{
ClapperMediaItem *item = NULL;
gboolean success;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
if (index != CLAPPER_QUEUE_INVALID_POSITION && index < self->items->len)
item = g_ptr_array_index (self->items, index);
if ((success = (index == CLAPPER_QUEUE_INVALID_POSITION
|| index < self->items->len))) {
if (_replace_current_item_unlocked (self, item, index))
_announce_current_item_and_index_change (self);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return success;
}
/**
* clapper_queue_select_next_item:
* @queue: a #ClapperQueue
*
* Selects next #ClapperMediaItem from @queue for playback.
*
* Note that this will try to select next item in the order
* of the queue, regardless of [enum@Clapper.QueueProgressionMode] set.
*
* Returns: %TRUE if there was another media item in queue, %FALSE otherwise.
*/
gboolean
clapper_queue_select_next_item (ClapperQueue *self)
{
gboolean success = FALSE;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION
&& self->current_index < self->items->len - 1) {
GST_DEBUG_OBJECT (self, "Selecting next queue item");
success = clapper_queue_select_index (self, self->current_index + 1);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return success;
}
/**
* clapper_queue_select_previous_item:
* @queue: a #ClapperQueue
*
* Selects previous #ClapperMediaItem from @queue for playback.
*
* Note that this will try to select previous item in the order
* of the queue, regardless of [enum@Clapper.QueueProgressionMode] set.
*
* Returns: %TRUE if there was previous media item in queue, %FALSE otherwise.
*/
gboolean
clapper_queue_select_previous_item (ClapperQueue *self)
{
gboolean success = FALSE;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION
&& self->current_index > 0) {
GST_DEBUG_OBJECT (self, "Selecting previous queue item");
success = clapper_queue_select_index (self, self->current_index - 1);
}
CLAPPER_QUEUE_REC_UNLOCK (self);
return success;
}
/**
* clapper_queue_get_item: (skip)
* @queue: a #ClapperQueue
* @index: an item index
*
* Get the #ClapperMediaItem at index.
*
* This behaves the same as [method@Gio.ListModel.get_item], and is here
* for code uniformity and convenience to avoid type casting by user.
*
* This function is not available in bindings as they already
* inherit `get_item()` method from [iface@Gio.ListModel] interface.
*
* Returns: (transfer full) (nullable): The #ClapperMediaItem at @index.
*/
ClapperMediaItem *
clapper_queue_get_item (ClapperQueue *self, guint index)
{
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL);
return g_list_model_get_item (G_LIST_MODEL (self), index);
}
/**
* clapper_queue_get_current_item:
* @queue: a #ClapperQueue
*
* Get the currently selected #ClapperMediaItem.
*
* Returns: (transfer full) (nullable): The current #ClapperMediaItem.
*/
ClapperMediaItem *
clapper_queue_get_current_item (ClapperQueue *self)
{
ClapperMediaItem *item = NULL;
/* XXX: For updating media item during playback we should
* use `player->played_item` instead to not be racy when
* changing and updating current item at the same time */
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL);
CLAPPER_QUEUE_REC_LOCK (self);
if (self->current_item)
item = gst_object_ref (self->current_item);
CLAPPER_QUEUE_REC_UNLOCK (self);
return item;
}
/**
* clapper_queue_get_current_index:
* @queue: a #ClapperQueue
*
* Get index of the currently selected #ClapperMediaItem.
*
* Returns: Current item index or [const@Clapper.QUEUE_INVALID_POSITION]
* when nothing is selected.
*/
guint
clapper_queue_get_current_index (ClapperQueue *self)
{
guint index;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), CLAPPER_QUEUE_INVALID_POSITION);
CLAPPER_QUEUE_REC_LOCK (self);
index = self->current_index;
CLAPPER_QUEUE_REC_UNLOCK (self);
return index;
}
/**
* clapper_queue_item_is_current:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem to check
*
* Checks if given #ClapperMediaItem is currently selected.
*
* Returns: %TRUE if @item is a current media item, %FALSE otherwise.
*/
gboolean
clapper_queue_item_is_current (ClapperQueue *self, ClapperMediaItem *item)
{
gboolean is_current;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (item), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
is_current = (item == self->current_item);
CLAPPER_QUEUE_REC_UNLOCK (self);
return is_current;
}
/**
* clapper_queue_find_item:
* @queue: a #ClapperQueue
* @item: a #ClapperMediaItem to search for
* @index: (optional) (out): return location for the index of
* the element, if found
*
* Get the index of #ClapperMediaItem within #ClapperQueue.
*
* Returns: %TRUE if @item is one of the elements of queue.
*/
gboolean
clapper_queue_find_item (ClapperQueue *self, ClapperMediaItem *item, guint *index)
{
gboolean found;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (item), FALSE);
CLAPPER_QUEUE_REC_LOCK (self);
found = g_ptr_array_find (self->items, item, index);
CLAPPER_QUEUE_REC_UNLOCK (self);
return found;
}
/**
* clapper_queue_get_n_items: (skip)
* @queue: a #ClapperQueue
*
* Get the number of items in #ClapperQueue.
*
* This behaves the same as [method@Gio.ListModel.get_n_items], and is here
* for code uniformity and convenience to avoid type casting by user.
*
* This function is not available in bindings as they already
* inherit get_n_items() method from #GListModel interface.
*
* Returns: The number of items in #ClapperQueue.
*/
guint
clapper_queue_get_n_items (ClapperQueue *self)
{
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), 0);
return g_list_model_get_n_items (G_LIST_MODEL (self));
}
/**
* clapper_queue_set_progression_mode:
* @queue: a #ClapperQueue
* @mode: a #ClapperQueueProgressionMode
*
* Set the #ClapperQueueProgressionMode of the #ClapperQueue.
*
* Changing the mode set will alter next item selection at the
* end of playback. For possible values and their descriptions,
* see #ClapperQueueProgressionMode documentation.
*/
void
clapper_queue_set_progression_mode (ClapperQueue *self, ClapperQueueProgressionMode mode)
{
gboolean changed;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
GST_OBJECT_LOCK (self);
if ((changed = self->progression_mode != mode))
self->progression_mode = mode;
GST_OBJECT_UNLOCK (self);
if (changed) {
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
/* Start shuffle from the current item, allowing
* reselecting past items already used without it */
if (mode == CLAPPER_QUEUE_PROGRESSION_SHUFFLE) {
CLAPPER_QUEUE_REC_LOCK (self);
_reset_shuffle_unlocked (self);
if (self->current_item)
clapper_media_item_set_used (self->current_item, TRUE);
CLAPPER_QUEUE_REC_UNLOCK (self);
}
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_PROGRESSION_MODE]);
if (player->reactables_manager)
clapper_reactables_manager_trigger_queue_progression_changed (player->reactables_manager, mode);
if (clapper_player_get_have_features (player))
clapper_features_manager_trigger_queue_progression_changed (player->features_manager, mode);
gst_object_unref (player);
}
}
/**
* clapper_queue_get_progression_mode:
* @queue: a #ClapperQueue
*
* Get the #ClapperQueueProgressionMode of the #ClapperQueue.
*
* Returns: a currently set #ClapperQueueProgressionMode.
*/
ClapperQueueProgressionMode
clapper_queue_get_progression_mode (ClapperQueue *self)
{
ClapperQueueProgressionMode mode;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), DEFAULT_PROGRESSION_MODE);
GST_OBJECT_LOCK (self);
mode = self->progression_mode;
GST_OBJECT_UNLOCK (self);
return mode;
}
/**
* clapper_queue_set_gapless:
* @queue: a #ClapperQueue
* @gapless: %TRUE to enable, %FALSE otherwise.
*
* Set #ClapperQueue progression to be gapless.
*
* Gapless playback will try to re-use as much as possible of underlying
* GStreamer elements when #ClapperQueue progresses, removing any
* potential gap in the data.
*
* Enabling this option mostly makes sense when used together with
* [property@Clapper.Queue:progression-mode] property set to
* [enum@Clapper.QueueProgressionMode.CONSECUTIVE].
*
* NOTE: This feature within GStreamer is rather new and
* might still cause playback issues. Disabled by default.
*/
void
clapper_queue_set_gapless (ClapperQueue *self, gboolean gapless)
{
gboolean changed;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
GST_OBJECT_LOCK (self);
if ((changed = self->gapless != gapless))
self->gapless = gapless;
GST_OBJECT_UNLOCK (self);
if (changed) {
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_GAPLESS]);
gst_object_unref (player);
}
}
/**
* clapper_queue_get_gapless:
* @queue: a #ClapperQueue
*
* Get if #ClapperQueue is set to use gapless progression.
*
* Returns: %TRUE if enabled, %FALSE otherwise.
*/
gboolean
clapper_queue_get_gapless (ClapperQueue *self)
{
gboolean gapless;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
GST_OBJECT_LOCK (self);
gapless = self->gapless;
GST_OBJECT_UNLOCK (self);
return gapless;
}
/**
* clapper_queue_set_instant:
* @queue: a #ClapperQueue
* @instant: %TRUE to enable, %FALSE otherwise.
*
* Set #ClapperQueue media item changes to be instant.
*
* Instant will try to re-use as much as possible of underlying
* GStreamer elements when #ClapperMediaItem is selected, allowing
* media item change requests to be faster.
*
* NOTE: This feature within GStreamer is rather new and
* might still cause playback issues. Disabled by default.
*/
void
clapper_queue_set_instant (ClapperQueue *self, gboolean instant)
{
gboolean changed;
g_return_if_fail (CLAPPER_IS_QUEUE (self));
GST_OBJECT_LOCK (self);
if ((changed = self->instant != instant))
self->instant = instant;
GST_OBJECT_UNLOCK (self);
if (changed) {
ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self));
clapper_app_bus_post_prop_notify (player->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_INSTANT]);
gst_object_unref (player);
}
}
/**
* clapper_queue_get_instant:
* @queue: a #ClapperQueue
*
* Get if #ClapperQueue is set to use instant media item changes.
*
* Returns: %TRUE if enabled, %FALSE otherwise.
*/
gboolean
clapper_queue_get_instant (ClapperQueue *self)
{
gboolean instant;
g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE);
GST_OBJECT_LOCK (self);
instant = self->instant;
GST_OBJECT_UNLOCK (self);
return instant;
}
static void
_item_remove_func (ClapperMediaItem *item)
{
gst_object_unparent (GST_OBJECT_CAST (item));
gst_object_unref (item);
}
static void
clapper_queue_init (ClapperQueue *self)
{
g_rec_mutex_init (&self->rec_lock);
self->items = g_ptr_array_new_with_free_func ((GDestroyNotify) _item_remove_func);
self->current_index = CLAPPER_QUEUE_INVALID_POSITION;
self->progression_mode = DEFAULT_PROGRESSION_MODE;
self->gapless = DEFAULT_GAPLESS;
self->instant = DEFAULT_INSTANT;
}
static void
clapper_queue_finalize (GObject *object)
{
ClapperQueue *self = CLAPPER_QUEUE_CAST (object);
GST_TRACE_OBJECT (self, "Finalize");
g_rec_mutex_clear (&self->rec_lock);
gst_clear_object (&self->current_item);
g_ptr_array_unref (self->items);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
clapper_queue_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
ClapperQueue *self = CLAPPER_QUEUE_CAST (object);
switch (prop_id) {
case PROP_CURRENT_ITEM:
g_value_take_object (value, clapper_queue_get_current_item (self));
break;
case PROP_CURRENT_INDEX:
g_value_set_uint (value, clapper_queue_get_current_index (self));
break;
case PROP_N_ITEMS:
g_value_set_uint (value, clapper_queue_get_n_items (self));
break;
case PROP_PROGRESSION_MODE:
g_value_set_enum (value, clapper_queue_get_progression_mode (self));
break;
case PROP_GAPLESS:
g_value_set_boolean (value, clapper_queue_get_gapless (self));
break;
case PROP_INSTANT:
g_value_set_boolean (value, clapper_queue_get_instant (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
clapper_queue_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
ClapperQueue *self = CLAPPER_QUEUE_CAST (object);
switch (prop_id) {
case PROP_CURRENT_INDEX:
clapper_queue_select_index (self, g_value_get_uint (value));
break;
case PROP_PROGRESSION_MODE:
clapper_queue_set_progression_mode (self, g_value_get_enum (value));
break;
case PROP_GAPLESS:
clapper_queue_set_gapless (self, g_value_get_boolean (value));
break;
case PROP_INSTANT:
clapper_queue_set_instant (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
clapper_queue_class_init (ClapperQueueClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperqueue", 0,
"Clapper Queue");
gobject_class->get_property = clapper_queue_get_property;
gobject_class->set_property = clapper_queue_set_property;
gobject_class->finalize = clapper_queue_finalize;
/**
* ClapperQueue:current-item:
*
* Currently selected media item for playback.
*/
param_specs[PROP_CURRENT_ITEM] = g_param_spec_object ("current-item",
NULL, NULL, CLAPPER_TYPE_MEDIA_ITEM,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperQueue:current-index:
*
* Index of currently selected media item for playback.
*/
param_specs[PROP_CURRENT_INDEX] = g_param_spec_uint ("current-index",
NULL, NULL, 0, G_MAXUINT, CLAPPER_QUEUE_INVALID_POSITION,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperQueue:n-items:
*
* Number of media items in the queue.
*/
param_specs[PROP_N_ITEMS] = g_param_spec_uint ("n-items",
NULL, NULL, 0, G_MAXUINT, 0,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperQueue:progression-mode:
*
* Queue progression mode.
*/
param_specs[PROP_PROGRESSION_MODE] = g_param_spec_enum ("progression-mode",
NULL, NULL, CLAPPER_TYPE_QUEUE_PROGRESSION_MODE, DEFAULT_PROGRESSION_MODE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperQueue:gapless:
*
* Use gapless progression.
*/
param_specs[PROP_GAPLESS] = g_param_spec_boolean ("gapless",
NULL, NULL, DEFAULT_GAPLESS,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperQueue:instant:
*
* Use instant media item changes.
*/
param_specs[PROP_INSTANT] = g_param_spec_boolean ("instant",
NULL, NULL, DEFAULT_INSTANT,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
}