clapper: Add media caching via download to local storage

The aim here is to stream an online video/audio while also at the
same time download/cache it to disk (excluding adaptive content).

After download is complete, further playback and seeking are done using the
locally cached file. This functionality uses GStreamer "downloadbuffer" element.

Player will emit a signal with a local download location after it completes,
so application will know where downloaded file for media item is stored in
case it wants to reuse it in the future.

It is up to application to set download dir and later manage downloaded
content in it, removing files its not going to use on next application
run and any incomplete downloads.
This commit is contained in:
Rafał Dzięgiel
2024-04-28 20:59:44 +02:00
parent 8f4107aab6
commit f67d5bef2e
8 changed files with 350 additions and 4 deletions

View File

@@ -47,6 +47,8 @@ void clapper_app_bus_post_refresh_timeline (ClapperAppBus *app_bus, GstObject *s
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);
void clapper_app_bus_post_desc_with_details_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, const gchar *desc, const gchar *details);
void clapper_app_bus_post_error_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, GError *error, const gchar *debug_info);

View File

@@ -43,6 +43,7 @@ enum
CLAPPER_APP_BUS_STRUCTURE_REFRESH_STREAMS,
CLAPPER_APP_BUS_STRUCTURE_REFRESH_TIMELINE,
CLAPPER_APP_BUS_STRUCTURE_SIMPLE_SIGNAL,
CLAPPER_APP_BUS_STRUCTURE_OBJECT_DESC_SIGNAL,
CLAPPER_APP_BUS_STRUCTURE_DESC_WITH_DETAILS_SIGNAL,
CLAPPER_APP_BUS_STRUCTURE_ERROR_SIGNAL
};
@@ -53,6 +54,7 @@ static ClapperBusQuark _structure_quarks[] = {
{"refresh-streams", 0},
{"refresh-timeline", 0},
{"simple-signal", 0},
{"object-desc-signal", 0},
{"desc-with-details-signal", 0},
{"error-signal", 0},
{NULL, 0}
@@ -63,6 +65,7 @@ enum
CLAPPER_APP_BUS_FIELD_UNKNOWN = 0,
CLAPPER_APP_BUS_FIELD_PSPEC,
CLAPPER_APP_BUS_FIELD_SIGNAL_ID,
CLAPPER_APP_BUS_FIELD_OBJECT,
CLAPPER_APP_BUS_FIELD_DESC,
CLAPPER_APP_BUS_FIELD_DETAILS,
CLAPPER_APP_BUS_FIELD_ERROR,
@@ -73,6 +76,7 @@ static ClapperBusQuark _field_quarks[] = {
{"unknown", 0},
{"pspec", 0},
{"signal-id", 0},
{"object", 0},
{"desc", 0},
{"details", 0},
{"error", 0},
@@ -177,6 +181,37 @@ _handle_simple_signal_msg (GstMessage *msg, const GstStructure *structure)
g_signal_emit (_MESSAGE_SRC_GOBJECT (msg), signal_id, 0);
}
void
clapper_app_bus_post_object_desc_signal (ClapperAppBus *self,
GstObject *src, guint signal_id,
GstObject *object, const gchar *desc)
{
GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (OBJECT_DESC_SIGNAL),
_FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, signal_id,
_FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, object,
_FIELD_QUARK (DESC), G_TYPE_STRING, desc,
NULL);
gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure));
}
static inline void
_handle_object_desc_signal_msg (GstMessage *msg, const GstStructure *structure)
{
guint signal_id = 0;
GstObject *object = NULL;
gchar *desc = NULL;
gst_structure_id_get (structure,
_FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, &signal_id,
_FIELD_QUARK (OBJECT), GST_TYPE_OBJECT, &object,
_FIELD_QUARK (DESC), G_TYPE_STRING, &desc,
NULL);
g_signal_emit (_MESSAGE_SRC_GOBJECT (msg), signal_id, 0, object, desc);
gst_object_unref (object);
g_free (desc);
}
void
clapper_app_bus_post_desc_with_details_signal (ClapperAppBus *self,
GstObject *src, guint signal_id,
@@ -253,6 +288,8 @@ clapper_app_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G
_handle_refresh_timeline_msg (msg, structure);
else if (quark == _STRUCTURE_QUARK (SIMPLE_SIGNAL))
_handle_simple_signal_msg (msg, structure);
else if (quark == _STRUCTURE_QUARK (OBJECT_DESC_SIGNAL))
_handle_object_desc_signal_msg (msg, structure);
else if (quark == _STRUCTURE_QUARK (ERROR_SIGNAL))
_handle_error_signal_msg (msg, structure);
else if (quark == _STRUCTURE_QUARK (DESC_WITH_DETAILS_SIGNAL))

View File

@@ -37,6 +37,12 @@ void clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, Gst
G_GNUC_INTERNAL
gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus);
G_GNUC_INTERNAL
void clapper_media_item_set_cache_location (ClapperMediaItem *item, const gchar *location);
G_GNUC_INTERNAL
const gchar * clapper_media_item_get_playback_uri (ClapperMediaItem *item);
G_GNUC_INTERNAL
void clapper_media_item_set_used (ClapperMediaItem *item, gboolean used);

View File

@@ -50,6 +50,8 @@ struct _ClapperMediaItem
gchar *container_format;
gdouble duration;
gchar *cache_uri;
/* For shuffle */
gboolean used;
};
@@ -450,6 +452,41 @@ clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDisco
gst_object_unref (player);
}
/* XXX: Must be set from player thread */
inline void
clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location)
{
g_free (self->cache_uri);
self->cache_uri = g_filename_to_uri (location, NULL, NULL);
GST_DEBUG_OBJECT (self, "Set cache URI: \"%s\"", self->cache_uri);
}
/* XXX: Can only be read from player thread.
* Returns cache URI if available, item URI otherwise. */
inline const gchar *
clapper_media_item_get_playback_uri (ClapperMediaItem *self)
{
if (self->cache_uri) {
GFile *file = g_file_new_for_uri (self->cache_uri);
gboolean exists;
/* It is an app error if it removes files in non-stopped state,
* and this function is only called when starting playback */
exists = g_file_query_exists (file, NULL);
g_object_unref (file);
if (exists)
return self->cache_uri;
/* Do not test file existence next time */
GST_DEBUG_OBJECT (self, "Cleared cache URI for non-existing file: \"%s\"",
self->cache_uri);
g_clear_pointer (&self->cache_uri, g_free);
}
return self->uri;
}
void
clapper_media_item_set_used (ClapperMediaItem *self, gboolean used)
{
@@ -505,6 +542,8 @@ clapper_media_item_finalize (GObject *object)
gst_object_unparent (GST_OBJECT_CAST (self->timeline));
gst_object_unref (self->timeline);
g_free (self->cache_uri);
G_OBJECT_CLASS (parent_class)->finalize (object);
}

View File

@@ -827,6 +827,24 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player)
g_free (name);
g_free (details);
} else if (gst_message_has_name (msg, "GstCacheDownloadComplete")) {
const GstStructure *structure;
const gchar *location;
guint signal_id;
if (G_UNLIKELY (player->played_item == NULL))
return;
structure = gst_message_get_structure (msg);
location = gst_structure_get_string (structure, "location");
signal_id = g_signal_lookup ("download-complete", CLAPPER_TYPE_PLAYER);
GST_INFO_OBJECT (player, "Download complete: %s", location);
clapper_media_item_set_cache_location (player->played_item, location);
clapper_app_bus_post_object_desc_signal (player->app_bus,
GST_OBJECT_CAST (player), signal_id,
GST_OBJECT_CAST (player->played_item), location);
}
}

View File

@@ -95,6 +95,8 @@ struct _ClapperPlayer
gboolean video_enabled;
gboolean audio_enabled;
gboolean subtitles_enabled;
gchar *download_dir;
gboolean download_enabled;
gdouble audio_offset;
gdouble subtitle_offset;
};

View File

@@ -43,7 +43,7 @@
#include "clapper-playbin-bus-private.h"
#include "clapper-app-bus-private.h"
#include "clapper-queue-private.h"
#include "clapper-media-item.h"
#include "clapper-media-item-private.h"
#include "clapper-stream-list-private.h"
#include "clapper-stream-private.h"
#include "clapper-video-stream-private.h"
@@ -61,6 +61,7 @@
#define DEFAULT_VIDEO_ENABLED TRUE
#define DEFAULT_AUDIO_ENABLED TRUE
#define DEFAULT_SUBTITLES_ENABLED TRUE
#define DEFAULT_DOWNLOAD_ENABLED FALSE
#define GST_CAT_DEFAULT clapper_player_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
@@ -90,6 +91,8 @@ enum
PROP_VIDEO_ENABLED,
PROP_AUDIO_ENABLED,
PROP_SUBTITLES_ENABLED,
PROP_DOWNLOAD_DIR,
PROP_DOWNLOAD_ENABLED,
PROP_AUDIO_OFFSET,
PROP_SUBTITLE_OFFSET,
PROP_SUBTITLE_FONT_DESC,
@@ -99,6 +102,7 @@ enum
enum
{
SIGNAL_SEEK_DONE,
SIGNAL_DOWNLOAD_COMPLETE,
SIGNAL_MISSING_PLUGIN,
SIGNAL_WARNING,
SIGNAL_ERROR,
@@ -278,14 +282,15 @@ void
clapper_player_handle_playbin_flags_changed (ClapperPlayer *self, const GValue *value)
{
gint flags;
gboolean video_enabled, audio_enabled, subtitles_enabled;
gboolean video_changed, audio_changed, subtitles_changed;
gboolean video_enabled, audio_enabled, subtitles_enabled, download_enabled;
gboolean video_changed, audio_changed, subtitles_changed, download_changed;
flags = g_value_get_flags (value);
video_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_VIDEO) == CLAPPER_PLAYER_PLAY_FLAG_VIDEO);
audio_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_AUDIO) == CLAPPER_PLAYER_PLAY_FLAG_AUDIO);
subtitles_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_TEXT) == CLAPPER_PLAYER_PLAY_FLAG_TEXT);
download_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_DOWNLOAD) == CLAPPER_PLAYER_PLAY_FLAG_DOWNLOAD);
GST_OBJECT_LOCK (self);
@@ -295,6 +300,8 @@ clapper_player_handle_playbin_flags_changed (ClapperPlayer *self, const GValue *
self->audio_enabled = audio_enabled;
if ((subtitles_changed = self->subtitles_enabled != subtitles_enabled))
self->subtitles_enabled = subtitles_enabled;
if ((download_changed = self->download_enabled != download_enabled))
self->download_enabled = download_enabled;
GST_OBJECT_UNLOCK (self);
@@ -313,6 +320,11 @@ clapper_player_handle_playbin_flags_changed (ClapperPlayer *self, const GValue *
clapper_app_bus_post_prop_notify (self->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_SUBTITLES_ENABLED]);
}
if (download_changed) {
GST_INFO_OBJECT (self, "Download enabled: %s", (download_enabled) ? "yes" : "no");
clapper_app_bus_post_prop_notify (self->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_DOWNLOAD_ENABLED]);
}
}
void
@@ -438,7 +450,7 @@ clapper_player_set_pending_item (ClapperPlayer *self, ClapperMediaItem *pending_
/* Might be NULL (e.g. after queue is cleared) */
if (pending_item) {
uri = clapper_media_item_get_uri (pending_item);
uri = clapper_media_item_get_playback_uri (pending_item);
suburi = clapper_media_item_get_suburi (pending_item);
}
@@ -725,6 +737,50 @@ clapper_player_reset (ClapperPlayer *self, gboolean pending_dispose)
}
}
static inline gchar *
_make_download_template (ClapperPlayer *self)
{
gchar *download_template = NULL;
GST_OBJECT_LOCK (self);
if (self->download_enabled && self->download_dir) {
if (g_mkdir_with_parents (self->download_dir, 0755) == 0) {
download_template = g_build_filename (self->download_dir, "XXXXXX", NULL);
} else {
GST_ERROR_OBJECT (self, "Could not create download dir: \"%s\"", self->download_dir);
}
}
GST_OBJECT_UNLOCK (self);
return download_template;
}
static void
_element_setup_cb (GstElement *playbin, GstElement *element, ClapperPlayer *self)
{
GstElementFactory *factory = gst_element_get_factory (element);
if (G_UNLIKELY (factory == NULL))
return;
GST_INFO_OBJECT (self, "Element setup: %s", GST_OBJECT_NAME (factory));
if (strcmp (GST_OBJECT_NAME (factory), "downloadbuffer") == 0) {
gchar *download_template;
/* Only set props if we have download template */
if ((download_template = _make_download_template (self))) {
g_object_set (element,
"temp-template", download_template,
"temp-remove", FALSE,
NULL);
g_free (download_template);
}
}
}
static void
_about_to_finish_cb (GstElement *playbin, ClapperPlayer *self)
{
@@ -1505,6 +1561,106 @@ clapper_player_get_subtitles_enabled (ClapperPlayer *self)
return enabled;
}
/**
* clapper_player_set_download_dir:
* @player: a #ClapperPlayer
* @path: (type filename): the path of a directory to use for media downloads
*
* Set a directory that @player will use to store downloads.
*
* See [property@Clapper.Player:download-enabled] description for more
* info how this works.
*
* Since: 0.8
*/
void
clapper_player_set_download_dir (ClapperPlayer *self, const gchar *path)
{
gboolean changed;
g_return_if_fail (CLAPPER_IS_PLAYER (self));
g_return_if_fail (path != NULL);
GST_OBJECT_LOCK (self);
changed = g_set_str (&self->download_dir, path);
GST_OBJECT_UNLOCK (self);
if (changed) {
GST_INFO_OBJECT (self, "Current download dir: %s", path);
clapper_app_bus_post_prop_notify (self->app_bus,
GST_OBJECT_CAST (self), param_specs[PROP_DOWNLOAD_DIR]);
}
}
/**
* clapper_player_get_download_dir:
* @player: a #ClapperPlayer
*
* Get path to a directory set for media downloads.
*
* Returns: (type filename) (transfer full) (nullable): the path of a directory
* set for media downloads or %NULL if no directory was set yet.
*
* Since: 0.8
*/
gchar *
clapper_player_get_download_dir (ClapperPlayer *self)
{
gchar *download_dir;
g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL);
GST_OBJECT_LOCK (self);
download_dir = g_strdup (self->download_dir);
GST_OBJECT_UNLOCK (self);
return download_dir;
}
/**
* clapper_player_set_download_enabled:
* @player: a #ClapperPlayer
* @enabled: whether enabled
*
* Set whether player should attempt progressive download buffering.
*
* For this to actually work a [property@Clapper.Player:download-dir]
* must also be set.
*
* Since: 0.8
*/
void
clapper_player_set_download_enabled (ClapperPlayer *self, gboolean enabled)
{
g_return_if_fail (CLAPPER_IS_PLAYER (self));
clapper_playbin_bus_post_set_play_flag (self->bus, CLAPPER_PLAYER_PLAY_FLAG_DOWNLOAD, enabled);
}
/**
* clapper_player_get_download_enabled:
* @player: a #ClapperPlayer
*
* Get whether progressive download buffering is enabled.
*
* Returns: %TRUE if enabled, %FALSE otherwise.
*
* Since: 0.8
*/
gboolean
clapper_player_get_download_enabled (ClapperPlayer *self)
{
gboolean enabled;
g_return_val_if_fail (CLAPPER_IS_PLAYER (self), FALSE);
GST_OBJECT_LOCK (self);
enabled = self->download_enabled;
GST_OBJECT_UNLOCK (self);
return enabled;
}
/**
* clapper_player_set_audio_offset:
* @player: a #ClapperPlayer
@@ -1793,6 +1949,7 @@ clapper_player_thread_start (ClapperThreadedObject *threaded_object)
for (i = 0; playbin_watchlist[i]; ++i)
gst_element_add_property_notify_watch (self->playbin, playbin_watchlist[i], TRUE);
g_signal_connect (self->playbin, "element-setup", G_CALLBACK (_element_setup_cb), self);
g_signal_connect (self->playbin, "about-to-finish", G_CALLBACK (_about_to_finish_cb), self);
if (!self->use_playbin3) {
@@ -1866,6 +2023,7 @@ clapper_player_init (ClapperPlayer *self)
self->video_enabled = DEFAULT_VIDEO_ENABLED;
self->audio_enabled = DEFAULT_AUDIO_ENABLED;
self->subtitles_enabled = DEFAULT_SUBTITLES_ENABLED;
self->download_enabled = DEFAULT_DOWNLOAD_ENABLED;
}
static void
@@ -1924,6 +2082,8 @@ clapper_player_finalize (GObject *object)
gst_clear_object (&self->pending_item);
gst_clear_object (&self->played_item);
g_free (self->download_dir);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
@@ -1991,6 +2151,12 @@ clapper_player_get_property (GObject *object, guint prop_id,
case PROP_SUBTITLES_ENABLED:
g_value_set_boolean (value, clapper_player_get_subtitles_enabled (self));
break;
case PROP_DOWNLOAD_DIR:
g_value_take_string (value, clapper_player_get_download_dir (self));
break;
case PROP_DOWNLOAD_ENABLED:
g_value_set_boolean (value, clapper_player_get_download_enabled (self));
break;
case PROP_AUDIO_OFFSET:
g_value_set_double (value, clapper_player_get_audio_offset (self));
break;
@@ -2046,6 +2212,12 @@ clapper_player_set_property (GObject *object, guint prop_id,
case PROP_SUBTITLES_ENABLED:
clapper_player_set_subtitles_enabled (self, g_value_get_boolean (value));
break;
case PROP_DOWNLOAD_DIR:
clapper_player_set_download_dir (self, g_value_get_string (value));
break;
case PROP_DOWNLOAD_ENABLED:
clapper_player_set_download_enabled (self, g_value_get_boolean (value));
break;
case PROP_AUDIO_OFFSET:
clapper_player_set_audio_offset (self, g_value_get_double (value));
break;
@@ -2251,6 +2423,52 @@ clapper_player_class_init (ClapperPlayerClass *klass)
NULL, NULL, DEFAULT_SUBTITLES_ENABLED,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperPlayer:download-dir:
*
* A directory that @player will use to download network content
* when [property@Clapper.Player:download-enabled] is set to %TRUE.
*
* If directory at @path does not exist, it will be automatically created.
*
* Since: 0.8
*/
param_specs[PROP_DOWNLOAD_DIR] = g_param_spec_string ("download-dir",
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperPlayer:download-enabled:
*
* Whether progressive download buffering is enabled.
*
* If progressive download is enabled and [property@Clapper.Player:download-dir]
* is set, streamed network content will be cached to the disk space instead
* of memory whenever possible. This allows for faster seeking through
* currently played media.
*
* Not every type of content is download applicable. Mainly applies to
* web content that does not use adaptive streaming.
*
* Once data that media item URI points to is fully downloaded, player
* will emit [signal@Clapper.Player::download-complete] signal with a
* location of downloaded file.
*
* Playing again the exact same [class@Clapper.MediaItem] object that was
* previously fully downloaded will cause player to automatically use that
* cached file if it still exists, avoiding any further network requests.
*
* Please note that player will not delete nor manage downloaded content.
* It is up to application to cleanup data in created cache directory
* (e.g. before app exits), in order to remove any downloads that app
* is not going to use next time it is run and incomplete ones.
*
* Since: 0.8
*/
param_specs[PROP_DOWNLOAD_ENABLED] = g_param_spec_boolean ("download-enabled",
NULL, NULL, DEFAULT_DOWNLOAD_ENABLED,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* ClapperPlayer:audio-offset:
*
@@ -2288,6 +2506,22 @@ clapper_player_class_init (ClapperPlayerClass *klass)
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
0, NULL, NULL, NULL, G_TYPE_NONE, 0);
/**
* ClapperPlayer::download-complete:
* @player: a #ClapperPlayer
* @item: a #ClapperMediaItem
* @location: (type filename): a path to downloaded file
*
* Media was fully downloaded to local cache directory. This signal will
* be only emitted when progressive download buffering is enabled by
* setting [property@Clapper.Player:download-enabled] property to %TRUE.
*
* Since: 0.8
*/
signals[SIGNAL_DOWNLOAD_COMPLETE] = g_signal_new ("download-complete",
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
0, NULL, NULL, NULL, G_TYPE_NONE, 2, CLAPPER_TYPE_MEDIA_ITEM, G_TYPE_STRING);
/**
* ClapperPlayer::missing-plugin:
* @player: a #ClapperPlayer

View File

@@ -101,6 +101,14 @@ void clapper_player_set_subtitles_enabled (ClapperPlayer *player, gboolean enabl
gboolean clapper_player_get_subtitles_enabled (ClapperPlayer *player);
void clapper_player_set_download_dir (ClapperPlayer *player, const gchar *path);
gchar * clapper_player_get_download_dir (ClapperPlayer *player);
void clapper_player_set_download_enabled (ClapperPlayer *player, gboolean enabled);
gboolean clapper_player_get_download_enabled (ClapperPlayer *player);
void clapper_player_set_audio_offset (ClapperPlayer *player, gdouble offset);
gdouble clapper_player_get_audio_offset (ClapperPlayer *player);