Merge pull request #438 from Rafostar/download-cache

clapper: Add media caching via download to local storage
This commit is contained in:
Rafał Dzięgiel
2024-05-10 00:14:28 +02:00
committed by GitHub
9 changed files with 407 additions and 4 deletions

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import gi
gi.require_version('Adw', '1')
gi.require_version('Clapper', '0.0')
gi.require_version('ClapperGtk', '0.0')
gi.require_version('GLib', '2.0')
gi.require_version('Gtk', '4.0')
from gi.repository import Adw, Clapper, ClapperGtk, GLib, Gtk
import shutil
Clapper.init(None)
download_dir = GLib.build_filenamev([GLib.get_user_cache_dir(), "example_download_dir", None])
print('Using ceche directory: {0}'.format(download_dir))
def on_download_complete(player, item, location):
# Media downloaded. Data from this file is still used for current playback (including seeking).
print('Download complete: {0} => {1}'.format(item.props.uri, location))
def on_activate(app):
win = Gtk.ApplicationWindow(application=app, default_width=640, default_height=396)
video = ClapperGtk.Video()
controls = ClapperGtk.SimpleControls(fullscreenable=False)
# Enable local storage caching and monitor it
video.props.player.set_download_dir(download_dir)
video.props.player.set_download_enabled(True)
video.props.player.connect('download-complete', on_download_complete)
# Configure playback
video.props.player.props.queue.set_progression_mode(Clapper.QueueProgressionMode.CAROUSEL)
video.props.player.set_autoplay(True)
# Create and add media for playback
item_1 = Clapper.MediaItem(uri='http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4')
item_2 = Clapper.MediaItem(uri='http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4')
video.props.player.props.queue.add_item(item_1)
video.props.player.props.queue.add_item(item_2)
# Assemble window
video.add_fading_overlay(controls)
win.set_child(video)
win.present()
# Create a new application
app = Adw.Application(application_id='com.example.ClapperDownloadCache')
app.connect('activate', on_activate)
# Run the application
app.run(None)
# Finally app should cleanup before exit. Possibly moving data to
# another dir if it wants to use it on next run and deleting what's
# left (so any unfinished downloads will also be removed).
print('Cleanup')
shutil.rmtree(download_dir)

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);