2 Commits

Author SHA1 Message Date
Rafał Dzięgiel
2076309aaa flatpak: Build gtuber 2021-10-18 15:32:55 +02:00
Rafał Dzięgiel
79618edd1e Port to new gtuber lib
Current YouTube code was broken for quite some time. Replace it with
the new Gtuber lib to make this code separate, independent and easier to maintain.
2021-10-18 15:28:34 +02:00
84 changed files with 1516 additions and 3366 deletions

View File

@@ -11,7 +11,7 @@ jobs:
name: "Flatpak"
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-41
image: bilelmoussaoui/flatpak-github-actions:gnome-40
options: --privileged
strategy:
matrix:

View File

@@ -6,4 +6,4 @@ imports.package.init({
prefix: '@prefix@',
libdir: '@libdir@',
});
imports.package.run(imports.src.main);
imports.package.run(imports.src.main@ID_POSTFIX@);

View File

@@ -1,19 +1,25 @@
bin_conf = configuration_data()
clapper_apps = ['', 'Remote', 'Daemon']
bin_conf.set('GJS', find_program('gjs').path())
bin_conf.set('PACKAGE_NAME', meson.project_name())
bin_conf.set('PACKAGE_VERSION', meson.project_version())
bin_conf.set('prefix', get_option('prefix'))
bin_conf.set('libdir', libdir)
foreach id_postfix : clapper_apps
app_postfix = (id_postfix != '') ? '.' + id_postfix : ''
configure_file(
input: 'com.github.rafostar.Clapper.in',
output: 'com.github.rafostar.Clapper',
configuration: bin_conf,
install: true,
install_dir: bindir,
install_mode: 'rwxr-xr-x'
)
bin_conf = configuration_data()
bin_conf.set('GJS', find_program('gjs').path())
bin_conf.set('PACKAGE_NAME', meson.project_name())
bin_conf.set('PACKAGE_VERSION', meson.project_version())
bin_conf.set('ID_POSTFIX', id_postfix)
bin_conf.set('prefix', get_option('prefix'))
bin_conf.set('libdir', libdir)
configure_file(
input: 'com.github.rafostar.Clapper.in',
output: 'com.github.rafostar.Clapper' + app_postfix,
configuration: bin_conf,
install: true,
install_dir: bindir,
install_mode: 'rwxr-xr-x'
)
endforeach
clapper_symlink_cmd = 'ln -fs @0@ $DESTDIR@1@'.format(
'com.github.rafostar.Clapper',

View File

@@ -10,9 +10,6 @@ scrolledwindow scrollbar.vertical slider {
}
/* Adwaita is missing osd ListBox */
.clapperplaylist {
background: none;
}
.clapperplaylist row {
border-radius: 5px;
}
@@ -31,6 +28,9 @@ scrolledwindow scrollbar.vertical slider {
margin-left: 2px;
margin-right: 2px;
}
.osd .clapperplaylist {
background: none;
}
.osd .clapperplaylist row image {
-gtk-icon-shadow: none;
}
@@ -223,9 +223,6 @@ scale trough slider {
.fullscreen.tvmode .positionscale marks.bottom {
margin-top: 2px;
}
.fullscreen.tvmode .positionscale trough {
border-radius: 3px;
}
.fullscreen.tvmode .positionscale trough highlight {
border-radius: 3px;
min-height: 20px;

View File

@@ -103,6 +103,16 @@
<summary>Set PlayFlags for playbin</summary>
</key>
<!-- Gtuber -->
<key name="yt-adaptive-enabled" type="b">
<default>false</default>
<summary>Enable to use adaptive streaming</summary>
</key>
<key name="yt-quality-type" type="i">
<default>1</default>
<summary>Max online video quality type</summary>
</key>
<!-- Other -->
<key name="window-size" type="s">
<default>'[800, 490]'</default>

View File

@@ -48,34 +48,6 @@
</screenshot>
</screenshots>
<releases>
<release version="0.4.1" date="2021-12-20">
<description>
<p>Fixes:</p>
<ul>
<li>Compatibility with more recent libadwaita versions</li>
<li>Toggle mute with M button alone</li>
<li>Allow handling YouTube with external GStreamer plugins</li>
<li>Fix catching errors when reading clipboard</li>
<li>Fix missing translator-credits</li>
<li>Fix missing gio-unix-2.0 dep</li>
<li>Fix playback pausing when entering fullscreen with touchscreen</li>
<li>Fix GST_PLUGIN_FEATURE_RANK env usage</li>
<li>Fix video/audio decoder change detection</li>
<li>Merge global video tags instead replacing them</li>
<li>Few other misc bug fixes</li>
</ul>
<p>New translations:</p>
<ul>
<li>Chinese Simplified</li>
<li>Czech</li>
<li>Hungarian</li>
<li>Portuguese</li>
<li>Portuguese, Brazilian</li>
<li>Russian</li>
<li>Spanish</li>
</ul>
</description>
</release>
<release version="0.4.0" date="2021-09-12">
<description>
<p>Changes:</p>

View File

@@ -152,7 +152,7 @@ struct _GstClapper
GstBus *bus;
GstState target_state, current_state;
gboolean is_live;
GSource *tick_source;
GSource *tick_source, *ready_timeout_source;
GstClockTime cached_duration;
gdouble rate;
@@ -253,9 +253,6 @@ static void gst_clapper_audio_info_update (GstClapper * self,
static void gst_clapper_subtitle_info_update (GstClapper * self,
GstClapperStreamInfo * stream_info);
static gboolean find_active_decoder_with_stream_id (GstClapper * self,
GstElementFactoryListType type, const gchar * stream_id);
/* For playbin3 */
static void gst_clapper_streams_info_create_from_collection (GstClapper * self,
GstClapperMediaInfo * media_info, GstStreamCollection * collection);
@@ -1138,6 +1135,44 @@ remove_tick_source (GstClapper * self)
self->tick_source = NULL;
}
static gboolean
ready_timeout_cb (gpointer user_data)
{
GstClapper *self = user_data;
if (self->target_state <= GST_STATE_READY) {
GST_DEBUG_OBJECT (self, "Setting pipeline to NULL state");
self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_NULL;
gst_element_set_state (self->playbin, GST_STATE_NULL);
}
return G_SOURCE_REMOVE;
}
static void
add_ready_timeout_source (GstClapper * self)
{
if (self->ready_timeout_source)
return;
self->ready_timeout_source = g_timeout_source_new_seconds (60);
g_source_set_callback (self->ready_timeout_source,
(GSourceFunc) ready_timeout_cb, self, NULL);
g_source_attach (self->ready_timeout_source, self->context);
}
static void
remove_ready_timeout_source (GstClapper * self)
{
if (!self->ready_timeout_source)
return;
g_source_destroy (self->ready_timeout_source);
g_source_unref (self->ready_timeout_source);
self->ready_timeout_source = NULL;
}
typedef struct
{
GstClapper *clapper;
@@ -1182,6 +1217,7 @@ emit_error (GstClapper * self, GError * err)
g_error_free (err);
remove_tick_source (self);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_NULL;
@@ -1826,17 +1862,6 @@ media_info_update (GstClapper * self, GstClapperMediaInfo * info)
"image_sample: %p", info->title, info->container, info->image_sample);
}
static void
merge_tags (GstTagList **my_tags, GstTagList *tags)
{
if (*my_tags) {
*my_tags = gst_tag_list_make_writable (*my_tags);
gst_tag_list_insert (*my_tags, tags, GST_TAG_MERGE_REPLACE);
} else {
*my_tags = gst_tag_list_ref (tags);
}
}
static void
tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
@@ -1852,12 +1877,17 @@ tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) {
g_mutex_lock (&self->lock);
if (self->media_info) {
merge_tags (&self->media_info->tags, tags);
if (self->media_info->tags)
gst_tag_list_unref (self->media_info->tags);
self->media_info->tags = gst_tag_list_ref (tags);
media_info_update (self, self->media_info);
g_mutex_unlock (&self->lock);
} else {
merge_tags (&self->global_tags, tags);
if (self->global_tags)
gst_tag_list_unref (self->global_tags);
self->global_tags = gst_tag_list_ref (tags);
g_mutex_unlock (&self->lock);
}
g_mutex_unlock (&self->lock);
}
gst_tag_list_unref (tags);
@@ -2020,7 +2050,6 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
{
GstClapper *self = GST_CLAPPER (user_data);
GstStreamCollection *collection = NULL;
gchar *video_sid, *audio_sid;
guint i, len;
gst_message_parse_streams_selected (msg, &collection);
@@ -2069,22 +2098,7 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
*current_sid = g_strdup (stream_id);
}
video_sid = g_strdup (self->video_sid);
audio_sid = g_strdup (self->audio_sid);
g_mutex_unlock (&self->lock);
if (video_sid) {
find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO, video_sid);
g_free (video_sid);
}
if (audio_sid) {
find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, audio_sid);
g_free (audio_sid);
}
}
static gboolean
@@ -2995,12 +3009,11 @@ decoder_changed_signal_data_free (DecoderChangedSignalData * data)
static void
emit_decoder_changed (GstClapper * self, gchar * decoder_name,
GstElementFactoryListType type)
gboolean is_video)
{
GstClapperSignalDispatcherFunc func = NULL;
if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) {
if (is_video) {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_VIDEO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_vdecoder, decoder_name) != 0) {
@@ -3008,8 +3021,7 @@ emit_decoder_changed (GstClapper * self, gchar * decoder_name,
g_free (self->last_vdecoder);
self->last_vdecoder = g_strdup (decoder_name);
}
} else if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) {
} else {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_AUDIO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_adecoder, decoder_name) != 0) {
@@ -3030,138 +3042,6 @@ emit_decoder_changed (GstClapper * self, gchar * decoder_name,
}
}
static gboolean
iterate_decoder_pads (GstClapper * self, GstElement * element,
const gchar * stream_id, GstElementFactoryListType type)
{
GstIterator *iter;
GValue value = { 0, };
gboolean found = FALSE;
iter = gst_element_iterate_src_pads (element);
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstPad *decoder_pad = g_value_get_object (&value);
gchar *decoder_stream_id = gst_pad_get_stream_id (decoder_pad);
GST_DEBUG_OBJECT (self, "Decoder stream: %s", decoder_stream_id);
/* In case of playbin3, pad may not be active yet */
if ((found = (g_strcmp0 (decoder_stream_id, stream_id) == 0
|| (!decoder_stream_id && self->use_playbin3)))) {
GstElementFactory *factory;
gchar *plugin_name;
factory = gst_element_get_factory (element);
plugin_name = gst_object_get_name (GST_OBJECT_CAST (factory));
if (plugin_name) {
GST_DEBUG_OBJECT (self, "Found decoder: %s", plugin_name);
emit_decoder_changed (self, plugin_name, type);
g_free (plugin_name);
}
}
g_free (decoder_stream_id);
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
return found;
}
static gboolean
find_active_decoder_with_stream_id (GstClapper * self, GstElementFactoryListType type,
const gchar * stream_id)
{
GstIterator *iter;
GValue value = { 0, };
gboolean found = FALSE;
GST_DEBUG_OBJECT (self, "Searching for decoder with stream: %s", stream_id);
iter = gst_bin_iterate_recurse (GST_BIN (self->playbin));
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstElement *element = g_value_get_object (&value);
GstElementFactory *factory = gst_element_get_factory (element);
if (factory && gst_element_factory_list_is_type (factory, type))
found = iterate_decoder_pads (self, element, stream_id, type);
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
return found;
}
static void
update_current_decoder (GstClapper *self, GstElementFactoryListType type)
{
GstIterator *iter;
GValue value = { 0, };
iter = gst_bin_iterate_all_by_element_factory_name (
GST_BIN (self->playbin), "input-selector");
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstElement *element = g_value_get_object (&value);
GstPad *active_pad;
gboolean found = FALSE;
g_object_get (G_OBJECT (element), "active-pad", &active_pad, NULL);
if (active_pad) {
gchar *stream_id;
stream_id = gst_pad_get_stream_id (active_pad);
gst_object_unref (active_pad);
if (stream_id) {
found = find_active_decoder_with_stream_id (self, type, stream_id);
g_free (stream_id);
}
}
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
}
static void
current_video_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
GstClapper * self)
{
GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO;
update_current_decoder (self, type);
}
static void
current_audio_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
GstClapper * self)
{
GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO;
update_current_decoder (self, type);
}
static void
element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
{
@@ -3174,6 +3054,13 @@ element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
if (plugin_name) {
GST_INFO_OBJECT (self, "Plugin setup: %s", plugin_name);
if (gst_element_factory_list_is_type (factory,
GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO))
emit_decoder_changed (self, plugin_name, TRUE);
else if (gst_element_factory_list_is_type (factory,
GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO))
emit_decoder_changed (self, plugin_name, FALSE);
/* TODO: Set plugin props */
}
g_free (plugin_name);
@@ -3342,11 +3229,6 @@ gst_clapper_main (gpointer data)
G_CALLBACK (audio_tags_changed_cb), self);
g_signal_connect (self->playbin, "text-tags-changed",
G_CALLBACK (subtitle_tags_changed_cb), self);
g_signal_connect (self->playbin, "notify::current-video",
G_CALLBACK (current_video_notify_cb), self);
g_signal_connect (self->playbin, "notify::current-audio",
G_CALLBACK (current_audio_notify_cb), self);
}
g_signal_connect (self->playbin, "notify::volume",
@@ -3373,6 +3255,7 @@ gst_clapper_main (gpointer data)
gst_object_unref (bus);
remove_tick_source (self);
remove_ready_timeout_source (self);
g_mutex_lock (&self->lock);
if (self->media_info) {
@@ -3441,104 +3324,6 @@ gst_clapper_has_plugin_with_features (const gchar * name)
return ret;
}
static gboolean
parse_feature_name (gchar * str, const gchar ** feature)
{
if (!str)
return FALSE;
g_strstrip (str);
if (str[0] != '\0') {
*feature = str;
return TRUE;
}
return FALSE;
}
static gboolean
parse_feature_rank (gchar * str, GstRank * rank)
{
if (!str)
return FALSE;
g_strstrip (str);
if (g_ascii_isdigit (str[0])) {
unsigned long l;
char *endptr;
l = strtoul (str, &endptr, 10);
if (endptr > str && endptr[0] == 0) {
*rank = (GstRank) l;
} else {
return FALSE;
}
} else if (g_ascii_strcasecmp (str, "NONE") == 0) {
*rank = GST_RANK_NONE;
} else if (g_ascii_strcasecmp (str, "MARGINAL") == 0) {
*rank = GST_RANK_MARGINAL;
} else if (g_ascii_strcasecmp (str, "SECONDARY") == 0) {
*rank = GST_RANK_SECONDARY;
} else if (g_ascii_strcasecmp (str, "PRIMARY") == 0) {
*rank = GST_RANK_PRIMARY;
} else if (g_ascii_strcasecmp (str, "MAX") == 0) {
*rank = (GstRank) G_MAXINT;
} else {
return FALSE;
}
return TRUE;
}
static void
_env_feature_rank_update (void)
{
const gchar *env;
gchar **split, **walk;
env = g_getenv ("GST_PLUGIN_FEATURE_RANK");
if (!env)
return;
split = g_strsplit (env, ",", 0);
for (walk = split; *walk; walk++) {
if (strchr (*walk, ':')) {
gchar **values;
values = g_strsplit (*walk, ":", 2);
if (values[0] && values[1]) {
GstRank rank;
const gchar *name;
if (parse_feature_name (values[0], &name)
&& parse_feature_rank (values[1], &rank)) {
GstPluginFeature *feature;
feature = gst_registry_find_feature (gst_registry_get (), name,
GST_TYPE_ELEMENT_FACTORY);
if (feature) {
GstRank old_rank;
old_rank = gst_plugin_feature_get_rank (feature);
if (old_rank != rank) {
gst_plugin_feature_set_rank (feature, rank);
GST_DEBUG ("Updated rank from env: %i -> %i for %s", old_rank, rank, name);
}
gst_object_unref (feature);
}
}
}
g_strfreev (values);
}
}
g_strfreev (split);
}
static void
gst_clapper_prepare_gstreamer (void)
{
@@ -3561,9 +3346,6 @@ gst_clapper_prepare_gstreamer (void)
gst_clapper_set_feature_rank ("v4l2slvp8dec", GST_RANK_NONE);
}
/* After setting defaults, update them from ENV */
_env_feature_rank_update ();
gst_clapper_gstreamer_prepared = TRUE;
GST_DEBUG ("GStreamer plugins prepared");
}
@@ -3640,6 +3422,7 @@ gst_clapper_play_internal (gpointer user_data)
}
g_mutex_unlock (&self->lock);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_PLAYING;
if (self->current_state < GST_STATE_PAUSED)
@@ -3707,6 +3490,7 @@ gst_clapper_pause_internal (gpointer user_data)
tick_cb (self);
remove_tick_source (self);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_PAUSED;
@@ -3786,11 +3570,13 @@ gst_clapper_stop_internal (GstClapper * self, gboolean transient)
tick_cb (self);
remove_tick_source (self);
add_ready_timeout_source (self);
self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_NULL;
self->current_state = GST_STATE_READY;
self->is_live = FALSE;
gst_bus_set_flushing (self->bus, TRUE);
gst_element_set_state (self->playbin, GST_STATE_NULL);
gst_element_set_state (self->playbin, GST_STATE_READY);
gst_bus_set_flushing (self->bus, FALSE);
change_state (self, transient && self->app_state != GST_CLAPPER_STATE_STOPPED
? GST_CLAPPER_STATE_BUFFERING : GST_CLAPPER_STATE_STOPPED);

View File

@@ -34,13 +34,36 @@ gstclapper_defines = [
'-DGST_USE_UNSTABLE_API',
'-DHAVE_GTK_GL',
]
gtk_deps = [gstgl_dep, gstglproto_dep]
have_gtk_gl_windowing = false
if not get_option('lib')
subdir_done()
gtk4_dep = dependency('gtk4', required: true)
if not gtk4_dep.version().version_compare('>=4.0.0')
error('GTK4 version on this system is too old')
endif
if not gir.found()
error('Clapper lib requires GI bindings to be compiled')
if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx)
gtk_x11_dep = dependency('gtk4-x11', required: false)
if gtk_x11_dep.found()
gtk_deps += gtk_x11_dep
if gst_gl_have_platform_glx
gtk_deps += gstglx11_dep
endif
have_gtk_gl_windowing = true
endif
endif
if gst_gl_have_window_wayland and gst_gl_have_platform_egl
gtk_wayland_dep = dependency('gtk4-wayland', required: false)
if gtk_wayland_dep.found()
gtk_deps += [gtk_wayland_dep, gstglwayland_dep]
have_gtk_gl_windowing = true
endif
endif
if gst_gl_have_platform_egl
gtk_deps += gstglegl_dep
endif
if not have_gtk_gl_windowing

1
lib/gst/meson.build vendored
View File

@@ -1,2 +1 @@
subdir('clapper')
subdir('plugin')

View File

@@ -1,681 +0,0 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2020-2022 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 Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstclappersink.h"
#include "gstgtkutils.h"
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 0
#define DEFAULT_PAR_D 1
#define SINK_FORMATS \
"{ BGR, RGB, BGRA, RGBA, ABGR, ARGB, RGBx, BGRx, RGBA64_LE, RGBA64_BE, NV12 }"
#define GST_CLAPPER_GL_SINK_CAPS \
"video/x-raw(" GST_CAPS_FEATURE_MEMORY_GL_MEMORY "), " \
"format = (string)" SINK_FORMATS ", " \
"width = " GST_VIDEO_SIZE_RANGE ", " \
"height = " GST_VIDEO_SIZE_RANGE ", " \
"framerate = " GST_VIDEO_FPS_RANGE ", " \
"texture-target = (string) { 2D, external-oes } " \
" ; " \
"video/x-raw(" GST_CAPS_FEATURE_MEMORY_GL_MEMORY "," \
GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION "), " \
"format = (string)" SINK_FORMATS ", " \
"width = " GST_VIDEO_SIZE_RANGE ", " \
"height = " GST_VIDEO_SIZE_RANGE ", " \
"framerate = " GST_VIDEO_FPS_RANGE ", " \
"texture-target = (string) { 2D, external-oes } "
enum
{
PROP_0,
PROP_WIDGET,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_LAST
};
GST_DEBUG_CATEGORY (gst_debug_clapper_sink);
#define GST_CAT_DEFAULT gst_debug_clapper_sink
static GstStaticPadTemplate gst_clapper_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (
GST_VIDEO_CAPS_MAKE_WITH_FEATURES ("memory:DMABuf", SINK_FORMATS) ";"
GST_CLAPPER_GL_SINK_CAPS ";"
GST_VIDEO_CAPS_MAKE (SINK_FORMATS)));
static void gst_clapper_sink_navigation_interface_init (
GstNavigationInterface *iface);
#define gst_clapper_sink_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstClapperSink, gst_clapper_sink,
GST_TYPE_VIDEO_SINK,
G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION,
gst_clapper_sink_navigation_interface_init);
GST_DEBUG_CATEGORY_INIT (gst_debug_clapper_sink,
"clappersink", 0, "Clapper Sink"));
GST_ELEMENT_REGISTER_DEFINE (clappersink, "clappersink", GST_RANK_NONE,
GST_TYPE_CLAPPER_SINK);
static void gst_clapper_sink_finalize (GObject *object);
static void gst_clapper_sink_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *param_spec);
static void gst_clapper_sink_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *param_spec);
static gboolean gst_clapper_sink_propose_allocation (GstBaseSink *bsink,
GstQuery *query);
static gboolean gst_clapper_sink_start (GstBaseSink *bsink);
static gboolean gst_clapper_sink_stop (GstBaseSink *bsink);
static GstStateChangeReturn
gst_clapper_sink_change_state (GstElement *element, GstStateChange transition);
static void gst_clapper_sink_get_times (GstBaseSink *bsink, GstBuffer *buffer,
GstClockTime *start, GstClockTime *end);
static GstCaps * gst_clapper_sink_get_caps (GstBaseSink *bsink,
GstCaps *filter);
static gboolean gst_clapper_sink_set_caps (GstBaseSink *bsink,
GstCaps *caps);
static GstFlowReturn gst_clapper_sink_show_frame (GstVideoSink *bsink,
GstBuffer *buffer);
static void
gst_clapper_sink_class_init (GstClapperSinkClass *klass)
{
GObjectClass *gobject_class;
GstElementClass *gstelement_class;
GstBaseSinkClass *gstbasesink_class;
GstVideoSinkClass *gstvideosink_class;
gobject_class = (GObjectClass *) klass;
gstelement_class = (GstElementClass *) klass;
gstbasesink_class = (GstBaseSinkClass *) klass;
gstvideosink_class = (GstVideoSinkClass *) klass;
gobject_class->set_property = gst_clapper_sink_set_property;
gobject_class->get_property = gst_clapper_sink_get_property;
gobject_class->finalize = gst_clapper_sink_finalize;
//gst_gtk_install_shared_properties (gobject_class);
gstelement_class->change_state = gst_clapper_sink_change_state;
gstbasesink_class->get_caps = gst_clapper_sink_get_caps;
gstbasesink_class->set_caps = gst_clapper_sink_set_caps;
gstbasesink_class->get_times = gst_clapper_sink_get_times;
gstbasesink_class->propose_allocation = gst_clapper_sink_propose_allocation;
gstbasesink_class->start = gst_clapper_sink_start;
gstbasesink_class->stop = gst_clapper_sink_stop;
gstvideosink_class->show_frame = gst_clapper_sink_show_frame;
gst_element_class_set_metadata (gstelement_class,
"Clapper Video Sink",
"Sink/Video", "A GTK4 video sink used by Clapper media player",
"Rafał Dzięgiel <rafostar.github@gmail.com>");
gst_element_class_add_static_pad_template (gstelement_class,
&gst_clapper_sink_template);
}
static void
gst_clapper_sink_init (GstClapperSink *self)
{
GObjectClass *gobject_class;
gobject_class = (GObjectClass *) GST_CLAPPER_SINK_GET_CLASS (self);
/* HACK: install here instead of class init to avoid GStreamer
* plugin scanner GObject type conflicts with older GTK versions */
if (!g_object_class_find_property (gobject_class, "widget")) {
g_object_class_install_property (gobject_class, PROP_WIDGET,
g_param_spec_object ("widget", "GTK Widget",
"The GtkWidget to place in the widget hierarchy "
"(must only be get from the GTK main thread)",
GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}
self->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
self->par_n = DEFAULT_PAR_N;
self->par_d = DEFAULT_PAR_D;
}
static void
gst_clapper_sink_finalize (GObject *object)
{
GstClapperSink *self = GST_CLAPPER_SINK (object);
GST_TRACE ("Finalize");
GST_OBJECT_LOCK (self);
if (self->window && self->window_destroy_id)
g_signal_handler_disconnect (self->window, self->window_destroy_id);
//if (self->widget && self->widget_destroy_id)
// g_signal_handler_disconnect (self->widget, self->widget_destroy_id);
g_clear_object (&self->obj);
GST_OBJECT_UNLOCK (self);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
widget_destroy_cb (GtkWidget *widget, GstClapperSink *self)
{
GST_OBJECT_LOCK (self);
g_clear_object (&self->obj);
GST_OBJECT_UNLOCK (self);
}
static void
window_destroy_cb (GtkWidget *window, GstClapperSink *self)
{
GST_OBJECT_LOCK (self);
if (self->obj) {
if (self->widget_destroy_id) {
GtkWidget *widget;
widget = gtk_clapper_object_get_widget (self->obj);
g_signal_handler_disconnect (widget, self->widget_destroy_id);
self->widget_destroy_id = 0;
}
g_clear_object (&self->obj);
}
self->window = NULL;
GST_OBJECT_UNLOCK (self);
}
static GtkWidget *
gst_clapper_sink_get_widget (GstClapperSink *self)
{
if (G_UNLIKELY (self->obj == NULL)) {
/* Ensure GTK is initialized */
if (!gtk_init_check ()) {
GST_ERROR_OBJECT (self, "Could not ensure GTK initialization");
return NULL;
}
self->obj = gtk_clapper_object_new ();
/* Take the floating ref, otherwise the destruction of the container will
* make this widget disappear possibly before we are done. */
//g_object_ref_sink (self->obj);
//self->widget_destroy_id = g_signal_connect (widget,
// "destroy", G_CALLBACK (widget_destroy_cb), self);
/* Back pointer */
gtk_clapper_object_set_element (
GTK_CLAPPER_OBJECT (self->obj), GST_ELEMENT (self));
}
return gtk_clapper_object_get_widget (self->obj);
}
static void
gst_clapper_sink_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
GstClapperSink *self = GST_CLAPPER_SINK (object);
switch (prop_id) {
case PROP_WIDGET:{
GObject *widget = NULL;
GST_OBJECT_LOCK (self);
if (G_LIKELY (self->obj != NULL))
widget = G_OBJECT (gtk_clapper_object_get_widget (self->obj));
GST_OBJECT_UNLOCK (self);
if (G_UNLIKELY (widget == NULL)) {
widget = gst_gtk_invoke_on_main (
(GThreadFunc) gst_clapper_sink_get_widget, self);
}
g_value_set_object (value, widget);
break;
}
case PROP_FORCE_ASPECT_RATIO:
g_value_set_boolean (value, self->force_aspect_ratio);
break;
case PROP_PIXEL_ASPECT_RATIO:
gst_value_set_fraction (value, self->par_n, self->par_d);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_sink_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
GstClapperSink *self = GST_CLAPPER_SINK (object);
switch (prop_id) {
case PROP_FORCE_ASPECT_RATIO:
self->force_aspect_ratio = g_value_get_boolean (value);
break;
case PROP_PIXEL_ASPECT_RATIO:
self->par_n = gst_value_get_fraction_numerator (value);
self->par_d = gst_value_get_fraction_denominator (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_sink_navigation_send_event (GstNavigation *navigation,
GstStructure *structure)
{
GstClapperSink *sink = GST_CLAPPER_SINK_CAST (navigation);
GstEvent *event;
GST_TRACE_OBJECT (sink, "Navigation event: %" GST_PTR_FORMAT, structure);
event = gst_event_new_navigation (structure);
if (G_LIKELY (GST_IS_EVENT (event))) {
GstPad *pad;
pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));
if (G_LIKELY (GST_IS_PAD (pad))) {
if (!gst_pad_send_event (pad, gst_event_ref (event))) {
/* If upstream didn't handle the event we'll post a message with it
* for the application in case it wants to do something with it */
gst_element_post_message (GST_ELEMENT_CAST (sink),
gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event));
}
gst_object_unref (pad);
}
gst_event_unref (event);
}
}
static void
gst_clapper_sink_navigation_interface_init (GstNavigationInterface *iface)
{
iface->send_event = gst_clapper_sink_navigation_send_event;
}
static gboolean
gst_clapper_sink_propose_allocation (GstBaseSink *bsink, GstQuery *query)
{
GstClapperSink *self = GST_CLAPPER_SINK (bsink);
GstBufferPool *pool = NULL;
GstStructure *config;
GstCaps *caps;
GstVideoInfo info;
guint size;
gboolean need_pool;
GstStructure *allocation_meta = NULL;
gint display_width, display_height;
//if (!self->display || !self->context)
// return FALSE;
gst_query_parse_allocation (query, &caps, &need_pool);
if (!caps)
goto no_caps;
if (!gst_video_info_from_caps (&info, caps))
goto invalid_caps;
/* Normal size of a frame */
size = GST_VIDEO_INFO_SIZE (&info);
if (need_pool) {
GST_DEBUG_OBJECT (self, "Creating new pool");
pool = gst_buffer_pool_new ();
config = gst_buffer_pool_get_config (pool);
gst_buffer_pool_config_set_params (config, caps, size, 0, 0);
if (!gst_buffer_pool_set_config (pool, config)) {
gst_object_unref (pool);
goto config_failed;
}
}
/* We need at least 3 buffers because we keep around the current one
* for memory to stay valid during resizing and hold on to the pending one */
gst_query_add_allocation_pool (query, pool, size, 3, 0);
if (pool)
gst_object_unref (pool);
/* FIXME: Read calculated display sizes from widget */
display_width = GST_VIDEO_INFO_WIDTH (&info);
display_height = GST_VIDEO_INFO_HEIGHT (&info);
if (display_width != 0 && display_height != 0) {
GST_DEBUG_OBJECT (self, "Sending alloc query with size %dx%d",
display_width, display_height);
allocation_meta = gst_structure_new ("GstVideoOverlayCompositionMeta",
"width", G_TYPE_UINT, display_width,
"height", G_TYPE_UINT, display_height, NULL);
}
gst_query_add_allocation_meta (query,
GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, allocation_meta);
if (allocation_meta)
gst_structure_free (allocation_meta);
/* We also support various metadata */
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL);
return TRUE;
/* ERRORS */
no_caps:
GST_DEBUG_OBJECT (bsink, "No caps specified");
return FALSE;
invalid_caps:
GST_DEBUG_OBJECT (bsink, "Invalid caps specified");
return FALSE;
config_failed:
GST_DEBUG_OBJECT (bsink, "Failed to set config");
return FALSE;
}
static gboolean
gst_clapper_sink_start_on_main (GstClapperSink *self)
{
GtkWidget *widget;
/* Make sure widget is created */
if (!(widget = gst_clapper_sink_get_widget (self)))
return FALSE;
/* After this point, self->obj will always be set */
if (!GTK_IS_ROOT (gtk_widget_get_root (widget))) {
GtkWidget *toplevel, *parent;
gchar *win_title;
if ((parent = gtk_widget_get_parent (widget))) {
GtkWidget *temp_parent;
while ((temp_parent = gtk_widget_get_parent (parent)))
parent = temp_parent;
}
toplevel = (parent) ? parent : widget;
/* User did not add widget its own UI, let's popup a new GtkWindow to
* make "gst-launch-1.0" work. */
self->window = (GtkWindow *) gtk_window_new ();
win_title = g_strdup_printf ("Clapper Sink - GTK %u.%u.%u Window",
gtk_get_major_version (),
gtk_get_minor_version (),
gtk_get_micro_version ());
gtk_window_set_default_size (self->window, 640, 480);
gtk_window_set_title (self->window, win_title);
gtk_window_set_child (self->window, toplevel);
g_free (win_title);
self->window_destroy_id = g_signal_connect (self->window,
"destroy", G_CALLBACK (window_destroy_cb), self);
}
return TRUE;
}
static gboolean
gst_clapper_sink_start (GstBaseSink *bsink)
{
GstClapperSink *self = GST_CLAPPER_SINK (bsink);
GtkClapperObject *obj = NULL;
if (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
gst_clapper_sink_start_on_main, self)))
return FALSE;
//widget = GTK_CLAPPER_WIDGET (self->widget);
GST_OBJECT_LOCK (self);
if (self->obj)
obj = g_object_ref (self->obj);
GST_OBJECT_UNLOCK (self);
if (G_UNLIKELY (obj == NULL)) {
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
"Clapper widget does not exist"), (NULL));
return FALSE;
}
if (!gtk_clapper_object_init_winsys (obj)) {
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
"Failed to initialize OpenGL with GTK"), (NULL));
return FALSE;
}
/*
if (!clapper_sink->display)
clapper_sink->display = gtk_clapper_gl_widget_get_display (clapper_widget);
if (!clapper_sink->context)
clapper_sink->context = gtk_clapper_gl_widget_get_context (clapper_widget);
if (!clapper_sink->gtk_context)
clapper_sink->gtk_context = gtk_clapper_gl_widget_get_gtk_context (clapper_widget);
if (!clapper_sink->display || !clapper_sink->context || !clapper_sink->gtk_context) {
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
"Failed to retrieve OpenGL context from GTK"), (NULL));
return FALSE;
}
gst_gl_element_propagate_display_context (GST_ELEMENT (bsink),
clapper_sink->display);
*/
return TRUE;
}
static gboolean
gst_clapper_sink_stop_on_main (GstClapperSink *self)
{
if (self->window) {
gtk_window_destroy (self->window);
self->window = NULL;
//self->widget = NULL;
}
return TRUE;
}
static gboolean
gst_clapper_sink_stop (GstBaseSink *bsink)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
if (G_UNLIKELY (self->window != NULL)) {
return ! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
gst_clapper_sink_stop_on_main, self);
}
return TRUE;
}
static void
gst_gtk_window_show_all_and_unref (GtkWindow *window)
{
gtk_window_present (window);
g_object_unref (window);
}
static GstStateChangeReturn
gst_clapper_sink_change_state (GstElement *element, GstStateChange transition)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (element);
GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
GST_DEBUG_OBJECT (self, "Changing state: %s => %s",
gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
if (G_UNLIKELY (ret == GST_STATE_CHANGE_FAILURE))
return ret;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:{
GtkWindow *window = NULL;
GST_OBJECT_LOCK (self);
if (self->window)
window = g_object_ref (self->window);
GST_OBJECT_UNLOCK (self);
if (window) {
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
gst_gtk_window_show_all_and_unref, window);
}
break;
}
case GST_STATE_CHANGE_PAUSED_TO_READY:
GST_OBJECT_LOCK (self);
if (G_LIKELY (self->obj != NULL))
gtk_clapper_object_set_buffer (self->obj, NULL);
GST_OBJECT_UNLOCK (self);
break;
default:
break;
}
return ret;
}
static void
gst_clapper_sink_get_times (GstBaseSink *bsink, GstBuffer *buffer,
GstClockTime *start, GstClockTime *end)
{
if (GST_BUFFER_TIMESTAMP_IS_VALID (buffer)) {
*start = GST_BUFFER_TIMESTAMP (buffer);
if (GST_BUFFER_DURATION_IS_VALID (buffer)) {
*end = *start + GST_BUFFER_DURATION (buffer);
} else {
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
if (GST_VIDEO_INFO_FPS_N (&self->v_info) > 0) {
*end = *start + gst_util_uint64_scale_int (GST_SECOND,
GST_VIDEO_INFO_FPS_D (&self->v_info),
GST_VIDEO_INFO_FPS_N (&self->v_info));
}
}
}
}
static GstCaps *
gst_clapper_sink_get_caps (GstBaseSink *bsink, GstCaps *filter)
{
GstCaps *tmp = NULL;
GstCaps *result = NULL;
tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink));
if (filter) {
GST_DEBUG_OBJECT (bsink, "Intersecting with filter caps %" GST_PTR_FORMAT,
filter);
result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
gst_caps_unref (tmp);
} else {
result = tmp;
}
//result = gst_gl_overlay_compositor_add_caps (result);
GST_DEBUG_OBJECT (bsink, "Returning caps: %" GST_PTR_FORMAT, result);
return result;
}
static gboolean
gst_clapper_sink_set_caps (GstBaseSink *bsink, GstCaps *caps)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
GST_DEBUG ("Set caps: %" GST_PTR_FORMAT, caps);
GST_OBJECT_LOCK (self);
if (!gst_video_info_from_caps (&self->v_info, caps)) {
GST_OBJECT_UNLOCK (self);
return FALSE;
}
if (G_UNLIKELY (self->obj == NULL)) {
GST_OBJECT_UNLOCK (self);
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("%s", "Output widget was destroyed"), (NULL));
return FALSE;
}
if (!gtk_clapper_object_set_format (self->obj, &self->v_info)) {
GST_OBJECT_UNLOCK (self);
return FALSE;
}
GST_OBJECT_UNLOCK (self);
return TRUE;
}
static GstFlowReturn
gst_clapper_sink_show_frame (GstVideoSink *vsink, GstBuffer *buffer)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink);
GST_TRACE ("Rendering buffer: %p", buffer);
GST_OBJECT_LOCK (self);
if (G_UNLIKELY (self->obj == NULL)) {
GST_OBJECT_UNLOCK (self);
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("%s", "Output widget was destroyed"), (NULL));
return GST_FLOW_ERROR;
}
gtk_clapper_object_set_buffer (self->obj, buffer);
GST_OBJECT_UNLOCK (self);
return GST_FLOW_OK;
}

View File

@@ -1,74 +0,0 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2020-2022 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 Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/gstvideosink.h>
#include <gst/video/video.h>
#include "gtkclapperobject.h"
G_BEGIN_DECLS
#define GST_TYPE_CLAPPER_SINK (gst_clapper_sink_get_type ())
#define GST_IS_CLAPPER_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_SINK))
#define GST_IS_CLAPPER_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_SINK))
#define GST_CLAPPER_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_SINK, GstClapperSinkClass))
#define GST_CLAPPER_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_SINK, GstClapperSinkClass))
#define GST_CLAPPER_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_SINK, GstClapperSink))
#define GST_CLAPPER_SINK_CAST(obj) ((GstClapperSink*)(obj))
typedef struct _GstClapperSink GstClapperSink;
typedef struct _GstClapperSinkClass GstClapperSinkClass;
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperSink, gst_object_unref)
#endif
struct _GstClapperSink
{
GstVideoSink parent;
GstVideoInfo v_info;
GtkClapperObject *obj;
GtkWindow *window;
/* properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gint display_width, display_height;
gulong widget_destroy_id, window_destroy_id;
};
struct _GstClapperSinkClass
{
GstVideoSinkClass parent_class;
};
GST_ELEMENT_REGISTER_DECLARE (clappersink);
GType gst_clapper_sink_get_type (void);
G_END_DECLS

View File

@@ -1,71 +0,0 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2015 Thibault Saunier <tsaunier@gnome.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "gstgtkutils.h"
struct invoke_context
{
GThreadFunc func;
gpointer data;
GMutex lock;
GCond cond;
gboolean fired;
gpointer res;
};
static gboolean
gst_gtk_invoke_func (struct invoke_context *info)
{
g_mutex_lock (&info->lock);
info->res = info->func (info->data);
info->fired = TRUE;
g_cond_signal (&info->cond);
g_mutex_unlock (&info->lock);
return G_SOURCE_REMOVE;
}
gpointer
gst_gtk_invoke_on_main (GThreadFunc func, gpointer data)
{
GMainContext *main_context = g_main_context_default ();
struct invoke_context info;
g_mutex_init (&info.lock);
g_cond_init (&info.cond);
info.fired = FALSE;
info.func = func;
info.data = data;
g_main_context_invoke (main_context, (GSourceFunc) gst_gtk_invoke_func,
&info);
g_mutex_lock (&info.lock);
while (!info.fired)
g_cond_wait (&info.cond, &info.lock);
g_mutex_unlock (&info.lock);
g_mutex_clear (&info.lock);
g_cond_clear (&info.cond);
return info.res;
}

View File

@@ -1,26 +0,0 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2015 Thibault Saunier <tsaunier@gnome.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <glib.h>
gpointer gst_gtk_invoke_on_main (GThreadFunc func, gpointer data);

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) 2022 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 Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstclappersink.h"
static gboolean
plugin_init (GstPlugin *plugin)
{
gboolean res = FALSE;
res |= GST_ELEMENT_REGISTER (clappersink, plugin);
return res;
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR,
clapper, "Clapper elements", plugin_init, VERSION, "LGPL",
GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)

View File

@@ -1,960 +0,0 @@
/*
* GStreamer
* Copyright (C) 2022 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 Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gst/gl/gstglfuncs.h>
#include <gst/allocators/gstdmabuf.h>
#include "gtkclapperobject.h"
#include "gstgtkutils.h"
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
#include <gdk/x11/gdkx.h>
#if GST_GL_HAVE_PLATFORM_EGL
#include <gst/gl/egl/gstgldisplay_egl.h>
#endif
#endif
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
#include <gdk/wayland/gdkwayland.h>
#include <gst/gl/wayland/gstgldisplay_wayland.h>
#endif
#if GST_GL_HAVE_PLATFORM_EGL
#include <gst/gl/egl/gsteglimage.h>
#endif
GST_DEBUG_CATEGORY (gst_debug_clapper_object);
#define GST_CAT_DEFAULT gst_debug_clapper_object
static void gtk_clapper_object_paintable_iface_init (GdkPaintableInterface *iface);
static void gtk_clapper_object_finalize (GObject *object);
static const GLfloat vertices[] = {
1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 1.0f
};
static const GLushort indices[] = {
0, 1, 2, 0, 2, 3
};
/* GTK4 renders things upside down ¯\_(ツ)_/¯ */
static const gfloat vertical_flip_matrix[] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
#define gtk_clapper_object_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GtkClapperObject, gtk_clapper_object, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
gtk_clapper_object_paintable_iface_init)
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "gtkclapperobject", 0,
"GTK Clapper Object"));
static void
gtk_clapper_object_class_init (GtkClapperObjectClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
gobject_class->finalize = gtk_clapper_object_finalize;
}
static void
gtk_clapper_object_init (GtkClapperObject *self)
{
self->last_pos_x = 0;
self->last_pos_y = 0;
self->picture = (GtkPicture *) gtk_picture_new ();
/* We cannot do textures of 0x0px size */
gtk_widget_set_size_request (GTK_WIDGET (self->picture), 1, 1);
/* Center instead of fill to not draw empty space into framebuffer */
gtk_widget_set_halign (GTK_WIDGET (self->picture), GTK_ALIGN_CENTER);
gtk_widget_set_valign (GTK_WIDGET (self->picture), GTK_ALIGN_CENTER);
gtk_picture_set_paintable (self->picture, GDK_PAINTABLE (self));
gst_video_info_init (&self->v_info);
gst_video_info_init (&self->pending_v_info);
g_weak_ref_init (&self->element, NULL);
g_mutex_init (&self->lock);
self->gst_tex_target = GST_GL_TEXTURE_TARGET_EXTERNAL_OES;
self->gl_tex_target = gst_gl_texture_target_to_gl (self->gst_tex_target);
}
static void
gtk_clapper_object_finalize (GObject *object)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT (object);
if (self->draw_id)
g_source_remove (self->draw_id);
gst_buffer_replace (&self->pending_buffer, NULL);
gst_buffer_replace (&self->buffer, NULL);
g_mutex_clear (&self->lock);
g_weak_ref_clear (&self->element);
GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object));
}
static void
_gdk_gl_context_set_active (GtkClapperObject *self, gboolean activate)
{
/* We wrap around a GDK context, so we need to make
* both GTK and GStreamer aware of its active state */
if (activate) {
gdk_gl_context_make_current (self->gdk_context);
gst_gl_context_activate (self->wrapped_context, TRUE);
} else {
gst_gl_context_activate (self->wrapped_context, FALSE);
gdk_gl_context_clear_current ();
}
}
static GdkMemoryFormat
video_format_to_gdk_memory_format (GstVideoFormat format)
{
switch (format) {
case GST_VIDEO_FORMAT_BGR:
return GDK_MEMORY_B8G8R8;
case GST_VIDEO_FORMAT_RGB:
return GDK_MEMORY_R8G8B8;
case GST_VIDEO_FORMAT_BGRA:
return GDK_MEMORY_B8G8R8A8;
case GST_VIDEO_FORMAT_RGBA:
return GDK_MEMORY_R8G8B8A8;
case GST_VIDEO_FORMAT_ABGR:
return GDK_MEMORY_A8B8G8R8;
case GST_VIDEO_FORMAT_ARGB:
return GDK_MEMORY_A8R8G8B8;
case GST_VIDEO_FORMAT_BGRx:
return GDK_MEMORY_B8G8R8A8_PREMULTIPLIED;
case GST_VIDEO_FORMAT_RGBx:
return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
case GST_VIDEO_FORMAT_RGBA64_LE:
case GST_VIDEO_FORMAT_RGBA64_BE:
return GDK_MEMORY_R16G16B16A16_PREMULTIPLIED;
default:
g_assert_not_reached ();
}
/* Number not belonging to any format */
return GDK_MEMORY_N_FORMATS;
}
static void
gtk_clapper_object_bind_buffer (GtkClapperObject *self)
{
const GstGLFuncs *gl = self->wrapped_context->gl_vtable;
gl->BindBuffer (GL_ARRAY_BUFFER, self->vertex_buffer);
/* Load the vertex position */
gl->VertexAttribPointer (self->attr_position, 3, GL_FLOAT, GL_FALSE,
5 * sizeof (GLfloat), (void *) 0);
/* Load the texture coordinate */
gl->VertexAttribPointer (self->attr_texture, 2, GL_FLOAT, GL_FALSE,
5 * sizeof (GLfloat), (void *) (3 * sizeof (GLfloat)));
gl->EnableVertexAttribArray (self->attr_position);
gl->EnableVertexAttribArray (self->attr_texture);
}
static void
gtk_clapper_object_unbind_buffer (GtkClapperObject *self)
{
const GstGLFuncs *gl = self->wrapped_context->gl_vtable;
gl->BindBuffer (GL_ARRAY_BUFFER, 0);
gl->DisableVertexAttribArray (self->attr_position);
gl->DisableVertexAttribArray (self->attr_texture);
}
static void
gtk_clapper_object_init_redisplay (GtkClapperObject *self)
{
GstGLSLStage *frag_stage, *vert_stage;
GError *error = NULL;
gchar *frag_str;
const GstGLFuncs *gl;
if (self->gst_tex_target != GST_GL_TEXTURE_TARGET_EXTERNAL_OES)
return;
if (!((vert_stage = gst_glsl_stage_new_with_string (self->wrapped_context,
GL_VERTEX_SHADER, GST_GLSL_VERSION_NONE,
GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY,
gst_gl_shader_string_vertex_mat4_vertex_transform)))) {
GST_ERROR ("Failed to retrieve vertex shader for texture target");
return;
}
frag_str = gst_gl_shader_string_fragment_external_oes_get_default (
self->wrapped_context, GST_GLSL_VERSION_NONE,
GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY);
frag_stage = gst_glsl_stage_new_with_string (self->wrapped_context,
GL_FRAGMENT_SHADER, GST_GLSL_VERSION_NONE,
GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY, frag_str);
g_free (frag_str);
if (!frag_stage) {
GST_ERROR ("Failed to retrieve fragment shader for texture target");
return;
}
if (!((self->shader = gst_gl_shader_new_link_with_stages (self->wrapped_context,
&error, vert_stage, frag_stage, NULL)))) {
GST_ERROR ("Failed to initialize shader: %s", error->message);
g_clear_error (&error);
gst_object_unref (vert_stage);
gst_object_unref (frag_stage);
return;
}
self->attr_position =
gst_gl_shader_get_attribute_location (self->shader, "a_position");
self->attr_texture =
gst_gl_shader_get_attribute_location (self->shader, "a_texcoord");
gl = self->wrapped_context->gl_vtable;
if (gl->GenVertexArrays) {
gl->GenVertexArrays (1, &self->vao);
gl->BindVertexArray (self->vao);
}
gl->GenBuffers (1, &self->vertex_buffer);
gl->BindBuffer (GL_ARRAY_BUFFER, self->vertex_buffer);
gl->BufferData (GL_ARRAY_BUFFER, 4 * 5 * sizeof (GLfloat), vertices, GL_STATIC_DRAW);
if (gl->GenVertexArrays) {
gtk_clapper_object_bind_buffer (self);
gl->BindVertexArray (0);
}
gl->BindBuffer (GL_ARRAY_BUFFER, 0);
self->initiated = TRUE;
}
static gboolean
_dmabuf_into_texture (GtkClapperObject *self, gint *fds, gsize *offsets)
{
GstEGLImage *image;
const GstGLFuncs *gl;
image = gst_egl_image_from_dmabuf_direct_target (self->wrapped_context,
fds, offsets, &self->v_info, self->gst_tex_target);
/* If HW colorspace conversion failed and there is only one
* plane, we can just make it into single EGLImage as is */
if (!image && GST_VIDEO_INFO_N_PLANES (&self->v_info) == 1)
image = gst_egl_image_from_dmabuf (self->wrapped_context,
fds[0], &self->v_info, 0, offsets[0]);
/* Still no image? Give up then */
if (!image)
return FALSE;
gl = self->wrapped_context->gl_vtable;
if (!self->texture_id)
gl->GenTextures (1, &self->texture_id);
gl->BindTexture (self->gl_tex_target, self->texture_id);
gl->TexParameteri (self->gl_tex_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl->TexParameteri (self->gl_tex_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl->TexParameteri (self->gl_tex_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
gl->TexParameteri (self->gl_tex_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl->EGLImageTargetTexture2D (self->gl_tex_target, gst_egl_image_get_image (image));
gl->BindTexture (GL_TEXTURE_2D, 0);
gst_egl_image_unref (image);
return TRUE;
}
static gboolean
_ext_texture_into_2d (GtkClapperObject *self, guint tex_width, guint tex_height)
{
GLuint framebuffer, new_texture_id;
GLenum status;
const GstGLFuncs *gl;
if (!self->initiated)
gtk_clapper_object_init_redisplay (self);
gl = self->wrapped_context->gl_vtable;
gl->GenFramebuffers (1, &framebuffer);
gl->BindFramebuffer (GL_FRAMEBUFFER, framebuffer);
gl->GenTextures (1, &new_texture_id);
gl->BindTexture (GL_TEXTURE_2D, new_texture_id);
gl->TexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl->TexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl->TexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
gl->TexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl->TexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, tex_width, tex_height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, NULL);
gl->FramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, new_texture_id, 0);
status = gl->CheckFramebufferStatus (GL_FRAMEBUFFER);
if (G_UNLIKELY (status != GL_FRAMEBUFFER_COMPLETE)) {
GST_ERROR ("Invalid framebuffer status: %u", status);
gl->BindTexture (GL_TEXTURE_2D, 0);
gl->DeleteTextures (1, &new_texture_id);
gl->BindFramebuffer (GL_FRAMEBUFFER, 0);
gl->DeleteFramebuffers (1, &framebuffer);
return FALSE;
}
gl->Viewport (0, 0, tex_width, tex_height);
gst_gl_shader_use (self->shader);
if (gl->BindVertexArray)
gl->BindVertexArray (self->vao);
gtk_clapper_object_bind_buffer (self);
gl->ActiveTexture (GL_TEXTURE0);
gl->BindTexture (self->gl_tex_target, self->texture_id);
gst_gl_shader_set_uniform_1i (self->shader, "tex", 0);
gst_gl_shader_set_uniform_matrix_4fv (self->shader,
"u_transformation", 1, FALSE, vertical_flip_matrix);
gl->DrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
if (gl->BindVertexArray)
gl->BindVertexArray (0);
else
gtk_clapper_object_unbind_buffer (self);
gl->BindTexture (self->gl_tex_target, 0);
/* Replace external OES texture with new 2D one */
gl->DeleteTextures (1, &self->texture_id);
self->texture_id = new_texture_id;
gl->BindFramebuffer (GL_FRAMEBUFFER, 0);
gl->DeleteFramebuffers (1, &framebuffer);
return TRUE;
}
static GdkTexture *
gtk_clapper_object_import_dmabuf (GtkClapperObject *self, gint *fds, gsize *offsets)
{
GdkTexture *texture;
guint tex_width, tex_height;
_gdk_gl_context_set_active (self, TRUE);
if (!_dmabuf_into_texture (self, fds, offsets)) {
_gdk_gl_context_set_active (self, FALSE);
return NULL;
}
switch (self->gst_tex_target) {
case GST_GL_TEXTURE_TARGET_2D:
tex_width = GST_VIDEO_INFO_WIDTH (&self->v_info);
tex_height = GST_VIDEO_INFO_HEIGHT (&self->v_info);
break;
case GST_GL_TEXTURE_TARGET_EXTERNAL_OES:{
GtkWidget *widget = (GtkWidget *) self->picture;
gint scale;
scale = gtk_widget_get_scale_factor (widget);
tex_width = gtk_widget_get_width (widget) * scale;
tex_height = gtk_widget_get_height (widget) * scale;
if (G_LIKELY (_ext_texture_into_2d (self, tex_width, tex_height)))
break;
return NULL;
}
default:
g_assert_not_reached ();
return NULL;
}
texture = gdk_gl_texture_new (self->gdk_context,
self->texture_id, tex_width, tex_height, NULL, NULL);
_gdk_gl_context_set_active (self, FALSE);
return texture;
}
typedef gboolean (*MemTypeCheckFunc) (gpointer data);
static gboolean
buffer_memory_type_check (GstBuffer *buffer, MemTypeCheckFunc func)
{
guint i, n_mems;
n_mems = gst_buffer_n_memory (buffer);
for (i = 0; i < n_mems; i++) {
if (!func (gst_buffer_peek_memory (buffer, i)))
return FALSE;
}
return n_mems > 0;
}
static gboolean
verify_dmabuf_memory (GtkClapperObject *self, guint n_planes,
gint *fds, gsize *offsets)
{
guint i;
for (i = 0; i < n_planes; i++) {
GstMemory *memory;
gsize plane_size, mem_skip;
guint mem_idx, length;
plane_size = gst_gl_get_plane_data_size (&self->v_info, NULL, i);
if (!gst_buffer_find_memory (self->buffer,
GST_VIDEO_INFO_PLANE_OFFSET (&self->v_info, i),
plane_size, &mem_idx, &length, &mem_skip)) {
GST_DEBUG_OBJECT (self, "Could not find memory %u", i);
return FALSE;
}
/* We can't have more then one DMABuf per plane */
if (length != 1) {
GST_DEBUG_OBJECT (self, "Data for plane %u spans %u memories",
i, length);
return FALSE;
}
memory = gst_buffer_peek_memory (self->buffer, mem_idx);
offsets[i] = memory->offset + mem_skip;
fds[i] = gst_dmabuf_memory_get_fd (memory);
}
return TRUE;
}
static GdkTexture *
obtain_texture_from_current_buffer (GtkClapperObject *self)
{
GdkTexture *texture = NULL;
GstVideoFrame frame;
/* DMABuf */
if (buffer_memory_type_check (self->buffer, (MemTypeCheckFunc) gst_is_dmabuf_memory)) {
gsize offsets[GST_VIDEO_MAX_PLANES];
gint fds[GST_VIDEO_MAX_PLANES];
guint n_planes;
n_planes = GST_VIDEO_INFO_N_PLANES (&self->v_info);
if (!verify_dmabuf_memory (self, n_planes, fds, offsets)) {
GST_ERROR ("DMABuf memory is invalid");
return NULL;
}
if (!((texture = gtk_clapper_object_import_dmabuf (self, fds, offsets))))
GST_ERROR ("Could not create texture from DMABuf");
return texture;
}
/* GL Memory */
if (buffer_memory_type_check (self->buffer, (MemTypeCheckFunc) gst_is_gl_memory)) {
if (gst_video_frame_map (&frame, &self->v_info, self->buffer, GST_MAP_READ | GST_MAP_GL)) {
GST_FIXME_OBJECT (self, "Handle GstGLMemory");
texture = gdk_gl_texture_new (
self->gdk_context,
*(guint *) GST_VIDEO_FRAME_PLANE_DATA (&frame, 0),
GST_VIDEO_FRAME_WIDTH (&frame),
GST_VIDEO_FRAME_HEIGHT (&frame),
NULL, NULL);
gst_video_frame_unmap (&frame);
}
return texture;
}
/* RAW */
if (gst_video_frame_map (&frame, &self->v_info, self->buffer, GST_MAP_READ)) {
GBytes *bytes;
/* Our ref on a buffer together with 2 buffers pool ensures that
* current buffer will not be freed while another one is prepared */
bytes = g_bytes_new_with_free_func (
GST_VIDEO_FRAME_PLANE_DATA (&frame, 0),
GST_VIDEO_FRAME_HEIGHT (&frame) * GST_VIDEO_FRAME_PLANE_STRIDE (&frame, 0),
NULL, NULL);
texture = gdk_memory_texture_new (
GST_VIDEO_FRAME_WIDTH (&frame),
GST_VIDEO_FRAME_HEIGHT (&frame),
video_format_to_gdk_memory_format (GST_VIDEO_FRAME_FORMAT (&frame)),
bytes,
GST_VIDEO_FRAME_PLANE_STRIDE (&frame, 0));
g_bytes_unref (bytes);
gst_video_frame_unmap (&frame);
}
return texture;
}
static gboolean
calculate_display_par (GtkClapperObject *self, GstVideoInfo *info)
{
gboolean success;
gint width, height;
gint par_n, par_d;
gint display_par_n, display_par_d;
width = GST_VIDEO_INFO_WIDTH (info);
height = GST_VIDEO_INFO_HEIGHT (info);
par_n = GST_VIDEO_INFO_PAR_N (info);
par_d = GST_VIDEO_INFO_PAR_D (info);
if (!par_n)
par_n = 1;
/* User set props */
if (self->par_n != 0 && self->par_d != 0) {
display_par_n = self->par_n;
display_par_d = self->par_d;
} else {
display_par_n = 1;
display_par_d = 1;
}
if ((success = gst_video_calculate_display_ratio (&self->display_ratio_num,
&self->display_ratio_den, width, height, par_n, par_d, display_par_n,
display_par_d))) {
GST_LOG ("PAR: %u/%u, DAR: %u/%u", par_n, par_d, display_par_n, display_par_d);
}
return success;
}
static void
update_display_size (GtkClapperObject *self)
{
guint display_ratio_num, display_ratio_den;
gint width, height;
display_ratio_num = self->display_ratio_num;
display_ratio_den = self->display_ratio_den;
width = GST_VIDEO_INFO_WIDTH (&self->v_info);
height = GST_VIDEO_INFO_HEIGHT (&self->v_info);
if (height % display_ratio_den == 0) {
GST_DEBUG ("Keeping video height");
self->display_width = (guint)
gst_util_uint64_scale_int (height, display_ratio_num, display_ratio_den);
self->display_height = height;
} else if (width % display_ratio_num == 0) {
GST_DEBUG ("Keeping video width");
self->display_width = width;
self->display_height = (guint)
gst_util_uint64_scale_int (width, display_ratio_den, display_ratio_num);
} else {
GST_DEBUG ("Approximating while keeping video height");
self->display_width = (guint)
gst_util_uint64_scale_int (height, display_ratio_num, display_ratio_den);
self->display_height = height;
}
self->display_aspect_ratio = ((gdouble) self->display_width
/ (gdouble) self->display_height);
GST_DEBUG ("Scaling to %dx%d", self->display_width, self->display_height);
}
static void
update_paintable (GtkClapperObject *self, GdkPaintable *paintable)
{
/* No change, so discard the new one */
if (self->paintable == paintable) {
if (paintable)
g_object_unref (paintable);
return;
}
if (self->paintable)
g_object_unref (self->paintable);
self->paintable = paintable;
if (self->pending_resize) {
update_display_size (self);
gdk_paintable_invalidate_size ((GdkPaintable *) self);
self->pending_resize = FALSE;
}
gdk_paintable_invalidate_contents ((GdkPaintable *) self);
}
static gboolean
draw_on_main_cb (GtkClapperObject *self)
{
GdkTexture *texture;
GTK_CLAPPER_OBJECT_LOCK (self);
/* Replace used buffer and set matching v_info */
gst_buffer_replace (&self->buffer, self->pending_buffer);
self->v_info = self->pending_v_info;
texture = obtain_texture_from_current_buffer (self);
if (texture)
update_paintable (self, (GdkPaintable *) texture);
self->draw_id = 0;
GTK_CLAPPER_OBJECT_UNLOCK (self);
return G_SOURCE_REMOVE;
}
void
gtk_clapper_object_set_element (GtkClapperObject *self, GstElement *element)
{
g_weak_ref_set (&self->element, element);
}
gboolean
gtk_clapper_object_set_format (GtkClapperObject *self, GstVideoInfo *v_info)
{
GTK_CLAPPER_OBJECT_LOCK (self);
if (gst_video_info_is_equal (&self->pending_v_info, v_info)) {
GTK_CLAPPER_OBJECT_UNLOCK (self);
return TRUE;
}
if (!calculate_display_par (self, v_info)) {
GTK_CLAPPER_OBJECT_UNLOCK (self);
return FALSE;
}
self->pending_resize = TRUE;
self->pending_v_info = *v_info;
GTK_CLAPPER_OBJECT_UNLOCK (self);
return TRUE;
}
void
gtk_clapper_object_set_buffer (GtkClapperObject *self, GstBuffer *buffer)
{
GstVideoMeta *meta = NULL;
GTK_CLAPPER_OBJECT_LOCK (self);
gst_buffer_replace (&self->pending_buffer, buffer);
if (self->draw_id) {
GTK_CLAPPER_OBJECT_UNLOCK (self);
return;
}
if (self->pending_buffer)
meta = gst_buffer_get_video_meta (self->pending_buffer);
/* Update pending info from video meta */
if (meta) {
guint i;
GST_VIDEO_INFO_WIDTH (&self->pending_v_info) = meta->width;
GST_VIDEO_INFO_HEIGHT (&self->pending_v_info) = meta->height;
for (i = 0; i < meta->n_planes; i++) {
GST_VIDEO_INFO_PLANE_OFFSET (&self->pending_v_info, i) = meta->offset[i];
GST_VIDEO_INFO_PLANE_STRIDE (&self->pending_v_info, i) = meta->stride[i];
}
}
self->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT,
(GSourceFunc) draw_on_main_cb, self, NULL);
GTK_CLAPPER_OBJECT_UNLOCK (self);
}
GtkClapperObject *
gtk_clapper_object_new (void)
{
return g_object_new (GTK_TYPE_CLAPPER_OBJECT, NULL);
}
GtkWidget *
gtk_clapper_object_get_widget (GtkClapperObject *self)
{
return (GtkWidget *) self->picture;
}
static gboolean
wrap_current_gl (GstGLDisplay *display, GstGLPlatform platform, GstGLContext **context)
{
GstGLAPI gl_api = GST_GL_API_NONE;
guint gl_major = 0, gl_minor = 0;
gl_api = gst_gl_context_get_current_gl_api (platform, &gl_major, &gl_minor);
if (gl_api) {
const gboolean is_es = gl_api & (GST_GL_API_GLES1 | GST_GL_API_GLES2);
gchar *gl_api_str = gst_gl_api_to_string (gl_api);
guintptr gl_handle = 0;
GST_INFO ("Using GL API: %s, ver: %d.%d", gl_api_str, gl_major, gl_minor);
g_free (gl_api_str);
if (is_es && platform == GST_GL_PLATFORM_EGL && !g_getenv ("GST_GL_API")) {
GST_DEBUG ("No GST_GL_API env and GTK is using EGL GLES2, enforcing it");
gst_gl_display_filter_gl_api (display, GST_GL_API_GLES2);
}
gl_handle = gst_gl_context_get_current_gl_context (platform);
if (gl_handle) {
if ((*context = gst_gl_context_new_wrapped (display,
gl_handle, platform, gl_api)))
return TRUE;
}
}
return FALSE;
}
static void
retrieve_gl_context_on_main (GtkClapperObject *self)
{
GdkDisplay *gdk_display;
GstGLPlatform platform = GST_GL_PLATFORM_NONE;
GError *error = NULL;
gst_clear_object (&self->wrapped_context);
g_clear_object (&self->gdk_context);
gtk_widget_realize (GTK_WIDGET (self->picture));
if (!((self->gdk_context = gdk_surface_create_gl_context (gtk_native_get_surface (
gtk_widget_get_native (GTK_WIDGET (self->picture))), &error)))) {
GST_ERROR_OBJECT (self, "Error creating Gdk GL context: %s",
error ? error->message : "No error set by Gdk");
g_clear_error (&error);
return;
}
//gdk_gl_context_set_use_es (self->gdk_context, TRUE);
//gdk_gl_context_realize (self->gdk_context, &error);
gdk_display = gdk_gl_context_get_display (self->gdk_context);
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
if (GDK_IS_X11_DISPLAY (gdk_display)) {
gpointer display_ptr;
#if GST_GL_HAVE_PLATFORM_EGL && GTK_CHECK_VERSION(4,3,1)
display_ptr = gdk_x11_display_get_egl_display (gdk_display);
if (display_ptr)
self->display = (GstGLDisplay *)
gst_gl_display_egl_new_with_egl_display (display_ptr);
#endif
}
#endif
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
if (GDK_IS_WAYLAND_DISPLAY (gdk_display)) {
struct wl_display *wayland_display =
gdk_wayland_display_get_wl_display (gdk_display);
self->display = (GstGLDisplay *)
gst_gl_display_wayland_new_with_display (wayland_display);
}
#endif
if (G_UNLIKELY (!self->display)) {
GST_WARNING_OBJECT (self, "Unknown Gdk display!");
self->display = gst_gl_display_new ();
}
#if GST_GL_HAVE_PLATFORM_EGL
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
if (GST_IS_GL_DISPLAY_WAYLAND (self->display)) {
platform = GST_GL_PLATFORM_EGL;
GST_DEBUG ("Using EGL on Wayland");
goto have_platform;
}
#endif
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
if (GST_IS_GL_DISPLAY_EGL (self->display)) {
platform = GST_GL_PLATFORM_EGL;
GST_DEBUG ("Using EGL on x11");
goto have_platform;
}
#endif
#endif /* GST_GL_HAVE_PLATFORM_EGL */
GST_ERROR ("Unsupported GL platform");
return;
have_platform:
g_object_ref (self->gdk_context);
gdk_gl_context_make_current (self->gdk_context);
if (!wrap_current_gl (self->display, platform, &self->wrapped_context)) {
GST_WARNING ("Could not retrieve Gdk OpenGL context");
return;
}
GST_INFO ("Retrieved Gdk OpenGL context %" GST_PTR_FORMAT, self->wrapped_context);
gst_gl_context_activate (self->wrapped_context, TRUE);
if (!gst_gl_context_fill_info (self->wrapped_context, &error)) {
GST_ERROR ("Failed to retrieve Gdk context info: %s", error->message);
g_clear_error (&error);
g_clear_object (&self->wrapped_context);
}
/* Deactivate in both places */
_gdk_gl_context_set_active (self, FALSE);
}
gboolean
gtk_clapper_object_init_winsys (GtkClapperObject *self)
{
GError *error = NULL;
GTK_CLAPPER_OBJECT_LOCK (self);
if (self->display && self->gdk_context && self->wrapped_context) {
GST_TRACE ("Have already initialized contexts");
GTK_CLAPPER_OBJECT_UNLOCK (self);
return TRUE;
}
if (!self->wrapped_context) {
GTK_CLAPPER_OBJECT_UNLOCK (self);
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) retrieve_gl_context_on_main, self);
GTK_CLAPPER_OBJECT_LOCK (self);
}
if (!GST_IS_GL_CONTEXT (self->wrapped_context)) {
GST_FIXME ("Could not retrieve Gdk GL context");
GTK_CLAPPER_OBJECT_UNLOCK (self);
return FALSE;
}
GTK_CLAPPER_OBJECT_UNLOCK (self);
return TRUE;
}
/*
* GdkPaintableInterface
*/
static void
gtk_clapper_object_paintable_snapshot (GdkPaintable *paintable,
GdkSnapshot *snapshot, gdouble width, gdouble height)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT_CAST (paintable);
if (self->paintable)
gdk_paintable_snapshot (self->paintable, snapshot, width, height);
}
static GdkPaintable *
gtk_clapper_object_paintable_get_current_image (GdkPaintable *paintable)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT_CAST (paintable);
return (self->paintable)
? g_object_ref (self->paintable)
: gdk_paintable_new_empty (0, 0);
}
static gint
gtk_clapper_object_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT_CAST (paintable);
return self->display_width;
}
static gint
gtk_clapper_object_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT_CAST (paintable);
return self->display_height;
}
static gdouble
gtk_clapper_object_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
GtkClapperObject *self = GTK_CLAPPER_OBJECT_CAST (paintable);
return self->display_aspect_ratio;
}
static void
gtk_clapper_object_paintable_iface_init (GdkPaintableInterface *iface)
{
iface->snapshot = gtk_clapper_object_paintable_snapshot;
iface->get_current_image = gtk_clapper_object_paintable_get_current_image;
iface->get_intrinsic_width = gtk_clapper_object_paintable_get_intrinsic_width;
iface->get_intrinsic_height = gtk_clapper_object_paintable_get_intrinsic_height;
iface->get_intrinsic_aspect_ratio = gtk_clapper_object_paintable_get_intrinsic_aspect_ratio;
}

View File

@@ -1,139 +0,0 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2020-2022 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 Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/video.h>
#include <gst/gl/gl.h>
#include <gst/gl/gstglfuncs.h>
G_BEGIN_DECLS
#define GTK_TYPE_CLAPPER_OBJECT (gtk_clapper_object_get_type ())
#define GTK_IS_CLAPPER_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_CLAPPER_OBJECT))
#define GTK_IS_CLAPPER_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_CLAPPER_OBJECT))
#define GTK_CLAPPER_OBJECT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_CLAPPER_OBJECT, GtkClapperObjectClass))
#define GTK_CLAPPER_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_CLAPPER_OBJECT, GtkClapperObjectClass))
#define GTK_CLAPPER_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CLAPPER_OBJECT, GtkClapperObject))
#define GTK_CLAPPER_OBJECT_CAST(obj) ((GtkClapperObject*)(obj))
#define GTK_CLAPPER_OBJECT_LOCK(w) g_mutex_lock(&((GtkClapperObject*)(w))->lock)
#define GTK_CLAPPER_OBJECT_UNLOCK(w) g_mutex_unlock(&((GtkClapperObject*)(w))->lock)
typedef struct _GtkClapperObject GtkClapperObject;
typedef struct _GtkClapperObjectClass GtkClapperObjectClass;
struct _GtkClapperObject
{
GObject parent;
GtkPicture *picture;
GdkPaintable *paintable;
GstGLDisplay *display;
GdkGLContext *gdk_context;
GstGLContext *wrapped_context;
GstGLContext *gst_context;
/* properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gboolean keep_last_frame;
gint display_width;
gint display_height;
gdouble display_aspect_ratio;
/* Object dimensions */
gint scaled_width;
gint scaled_height;
/* Position coords */
gdouble last_pos_x;
gdouble last_pos_y;
gboolean negotiated;
gboolean ignore_buffers;
GstBuffer *pending_buffer;
GstBuffer *buffer;
GstVideoInfo pending_v_info;
GstVideoInfo v_info;
guint texture_id, oes_texture_id, next_texture_id;
/* resize */
gboolean pending_resize;
guint display_ratio_num;
guint display_ratio_den;
/*< private >*/
GMutex lock;
GWeakRef element;
/* event controllers */
GtkEventController *key_controller;
GtkEventController *motion_controller;
GtkGesture *click_gesture;
/* Pending draw idles callback */
guint draw_id;
GstGLTextureTarget gst_tex_target;
guint gl_tex_target;
GstGLShader *shader;
GLuint vao;
GLuint vertex_buffer;
GLint attr_position;
GLint attr_texture;
gboolean initiated;
guint frame_buffer;
};
struct _GtkClapperObjectClass
{
GObjectClass parent_class;
};
GType gtk_clapper_object_get_type (void);
GtkClapperObject * gtk_clapper_object_new (void);
GtkWidget * gtk_clapper_object_get_widget (GtkClapperObject *object);
gboolean gtk_clapper_object_init_winsys (GtkClapperObject *object);
gboolean gtk_clapper_object_set_format (GtkClapperObject *object, GstVideoInfo *v_info);
void gtk_clapper_object_set_buffer (GtkClapperObject *object, GstBuffer *buffer);
void gtk_clapper_object_set_element (GtkClapperObject *object, GstElement *element);
G_END_DECLS

View File

@@ -1,63 +0,0 @@
gst_plugins_libdir = join_paths(prefix, libdir, 'gstreamer-1.0')
gst_clapper_plugin_args = [
'-DHAVE_CONFIG_H',
'-DGST_USE_UNSTABLE_API',
]
gst_clapper_plugin_deps = [
gtk4_dep,
gst_dep,
gstbase_dep,
gstvideo_dep,
gstallocators_dep,
]
if get_option('default_library') == 'static'
gst_clapper_plugin_args += ['-DGST_STATIC_COMPILATION']
endif
gst_clapper_plugin_option = get_option('gst-plugin')
if gst_clapper_plugin_option.disabled()
subdir_done()
endif
foreach dep : gst_clapper_plugin_deps
if not dep.found()
if gst_clapper_plugin_option.enabled()
error('GStreamer plugin was enabled, but required dependencies were not found')
endif
subdir_done()
endif
endforeach
if not have_gtk_gl_windowing
if gst_clapper_plugin_option.enabled()
error('GTK4 widget requires GL windowing')
else
subdir_done()
endif
endif
if not gtk4_dep.version().version_compare('>=4.6.0')
if gst_clapper_plugin_option.enabled()
error('GTK4 version on this system is too old, plugin needs 4.6.0+')
else
subdir_done()
endif
endif
gst_clapper_plugin_sources = [
'gstclappersink.c',
'gtkclapperobject.c',
'gstgtkutils.c',
'gstplugin.c',
]
library('gstclapper',
gst_clapper_plugin_sources,
c_args: gst_clapper_plugin_args,
include_directories: configinc,
dependencies: gst_clapper_plugin_deps + gtk_deps,
install: true,
install_dir: gst_plugins_libdir,
)

44
lib/meson.build vendored
View File

@@ -1,5 +1,5 @@
glib_req = '>= 2.56.0'
gst_req = '>= 1.19.1'
gst_req = '>= 1.18.0'
api_version = '1.0'
libversion = meson.project_version()
@@ -200,8 +200,6 @@ gsttag_dep = dependency('gstreamer-tag-1.0', version: gst_req,
fallback: ['gst-plugins-base', 'tag_dep'])
gstvideo_dep = dependency('gstreamer-video-1.0', version: gst_req,
fallback: ['gst-plugins-base', 'video_dep'])
gstallocators_dep = dependency('gstreamer-allocators-1.0', version: gst_req,
fallback : ['gst-plugins-base', 'allocators_dep'])
# GStreamer OpenGL
gstgl_dep = dependency('gstreamer-gl-1.0', version: gst_req,
@@ -253,49 +251,21 @@ giounix_dep = dependency('gio-unix-2.0', version: glib_req, fallback: ['glib', '
cdata.set('DISABLE_ORC', 1)
cdata.set('GST_ENABLE_EXTRA_CHECKS', get_option('devel-checks'))
cdata.set_quoted('GST_PACKAGE_RELEASE_DATETIME', 'Unknown')
configinc = include_directories('.')
libsinc = include_directories('gst')
gir = find_program('g-ir-scanner')
gir = find_program('g-ir-scanner', required: true)
if not gir.found()
error('Clapper requires GI bindings to be compiled')
endif
gir_init_section = ['--add-init-section=extern void gst_init(gint*,gchar**);' + \
'g_setenv("GST_REGISTRY_1.0", "@0@", TRUE);'.format(meson.current_build_dir() + '/gir_empty_registry.reg') + \
'g_setenv("GST_PLUGIN_PATH_1_0", "", TRUE);' + \
'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \
'gst_init(NULL,NULL);', '--quiet'
]
gtk_deps = [gstgl_dep, gstglproto_dep]
have_gtk_gl_windowing = false
gtk4_dep = dependency('gtk4', required: true)
if not gtk4_dep.version().version_compare('>=4.0.0')
error('GTK4 version on this system is too old')
endif
if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx)
gtk_x11_dep = dependency('gtk4-x11', required: false)
if gtk_x11_dep.found()
gtk_deps += gtk_x11_dep
if gst_gl_have_platform_glx
gtk_deps += gstglx11_dep
endif
have_gtk_gl_windowing = true
endif
endif
if gst_gl_have_window_wayland and gst_gl_have_platform_egl
gtk_wayland_dep = dependency('gtk4-wayland', required: false)
if gtk_wayland_dep.found()
gtk_deps += [gtk_wayland_dep, gstglwayland_dep]
have_gtk_gl_windowing = true
endif
endif
if gst_gl_have_platform_egl
gtk_deps += gstglegl_dep
endif
subdir('gst')
configure_file(output: 'config.h', configuration: cdata)

View File

@@ -1,5 +1,5 @@
project('com.github.rafostar.Clapper', 'c', 'cpp',
version: '0.4.1',
version: '0.4.0',
meson_version: '>= 0.50.0',
license: 'GPL-3.0-or-later',
default_options: [
@@ -19,7 +19,7 @@ datadir = join_paths(get_option('prefix'), get_option('datadir'))
pkglibdir = join_paths(libdir, meson.project_name())
pkgdatadir = join_paths(datadir, meson.project_name())
if get_option('lib') or not get_option('gst-plugin').disabled()
if get_option('lib')
subdir('lib')
endif

View File

@@ -8,11 +8,6 @@ option('lib',
value: true,
description: 'Build GstClapper lib'
)
option('gst-plugin',
type: 'feature',
value: 'enabled',
description: 'Build GStreamer plugin (includes GTK video sink element)'
)
option('devel-checks',
type: 'boolean',
value: false,

View File

@@ -32,7 +32,6 @@
"flathub/lib/libdvdnav.json",
"flathub/lib/libass.json",
"flathub/lib/ffmpeg.json",
"testing/libsoup3.json",
"testing/gstreamer.json",
"testing/gtuber.json",
{

View File

@@ -1,7 +1,7 @@
{
"app-id": "com.github.rafostar.Clapper",
"runtime": "org.gnome.Platform",
"runtime-version": "41",
"runtime-version": "40",
"sdk": "org.gnome.Sdk",
"command": "com.github.rafostar.Clapper",
"finish-args": [
@@ -33,8 +33,13 @@
"flathub/lib/libass.json",
"flathub/lib/ffmpeg.json",
"flathub/lib/uchardet.json",
"testing/libsoup3.json",
"flathub/gstreamer-1.0/gstreamer.json",
"flathub/gstreamer-1.0/gst-plugins-base.json",
"flathub/gstreamer-1.0/gst-plugins-good.json",
"flathub/gstreamer-1.0/gst-plugins-bad.json",
"flathub/gstreamer-1.0/gst-plugins-ugly.json",
"flathub/gstreamer-1.0/gst-libav.json",
"flathub/gstreamer-1.0/gstreamer-vaapi.json",
"flathub/lib/gtk4.json",
"flathub/lib/libadwaita.json",
"testing/gtuber.json",

View File

@@ -23,7 +23,6 @@
"-Dintrospection=enabled",
"-Ddoc=disabled",
"-Dgtk_doc=disabled",
"-Dgpl=enabled",
"-Dgstreamer:benchmarks=disabled",
"-Dgstreamer:gobject-cast-checks=disabled",

View File

@@ -2,9 +2,7 @@
"name": "gtuber",
"buildsystem": "meson",
"config-opts": [
"-Dintrospection=disabled",
"-Dvapi=disabled",
"-Dgst-gtuber=enabled"
"-Dvapi=disabled"
],
"cleanup": [
"/include",

View File

@@ -1,24 +0,0 @@
{
"name": "libsoup3",
"buildsystem": "meson",
"config-opts": [
"-Dintrospection=enabled",
"-Dvapi=disabled",
"-Dtests=false",
"-Dsysprof=disabled",
"-Dhttp2_tests=disabled",
"-Dpkcs11_tests=disabled"
],
"cleanup": [
"/include",
"/lib/pkgconfig"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/libsoup.git",
"tag": "3.0.4",
"commit": "25a728020c4b53b5db4c4c675070e92f947fbd4d"
}
]
}

View File

@@ -24,10 +24,9 @@
%global gtk4_version 4.0.0
%global meson_version 0.50
%global glib2_version 2.56.0
%global soup_version 3.0.0
Name: clapper
Version: 0.4.1
Version: 0.4.0
Release: 1%{?dist}
Summary: Simple and modern GNOME media player
@@ -60,7 +59,6 @@ BuildRequires: gstreamer-plugins-base-devel >= %{gst_version}
BuildRequires: Mesa-libGLESv2-devel
BuildRequires: Mesa-libGLESv3-devel
Requires: libsoup-devel >= %{soup_version}
Requires: gstreamer >= %{gst_version}
Requires: gstreamer-plugins-base >= %{gst_version}
Requires: gstreamer-plugins-good >= %{gst_version}
@@ -82,7 +80,6 @@ BuildRequires: mesa-libGLES-devel
BuildRequires: mesa-libGLU-devel
BuildRequires: mesa-libEGL-devel
Requires: libsoup3-devel
Requires: gstreamer1 >= %{gst_version}
Requires: gstreamer1-plugins-base >= %{gst_version}
Requires: gstreamer1-plugins-good >= %{gst_version}
@@ -132,12 +129,6 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_libdir}/%{appname}/
%changelog
* Thu Feb 17 2022 Rafostar <rafostar.github@gmail.com> - 0.4.1-2
- Require libsoup3
* Mon Dec 20 2021 Rafostar <rafostar.github@gmail.com> - 0.4.1-1
- New version
* Sun Sep 12 2021 Rafostar <rafostar.github@gmail.com> - 0.4.0-1
- New version

View File

@@ -1 +1 @@
ca cs de es fr hu it nl pl pt pt_BR ru sv zh_CN
ca cs de es hu it nl pl pt_BR ru zh_CN

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Obre fitxers"
msgid "Open Files..."
msgstr "Obre fitxers..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Obre l'URI"
msgid "Open URI..."
msgstr "Obre l'URI..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,11 +18,11 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Otevřít soubory"
msgid "Open Files..."
msgstr "Otevřít soubory..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Otevřít URI"
msgid "Open URI..."
msgstr "Otevřít URI..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Dateien öffnen"
msgid "Open Files..."
msgstr "Dateien öffnen..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Webquelle öffnen"
msgid "Open URI..."
msgstr "Webquelle öffnen..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-16 16:58\n"
"PO-Revision-Date: 2021-09-14 15:25\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Abrir archivos"
msgid "Open Files..."
msgstr "Abrir archivos"
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Abrir URI"
msgid "Open URI..."
msgstr "Abrir URI"
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"
@@ -76,7 +76,7 @@ msgstr "Tocar"
#: ui/help-overlay.ui:39
msgid "Quit"
msgstr "Renunciar"
msgstr "Abandonar"
#: ui/help-overlay.ui:47
msgid "Media"
@@ -96,7 +96,7 @@ msgstr "Lista de reproducción"
#: ui/help-overlay.ui:67
msgid "Next item"
msgstr "Elemento siguiente"
msgstr "Artículo siguiente"
#: ui/help-overlay.ui:68
msgid "Double tap (right side)"
@@ -104,7 +104,7 @@ msgstr "Tocar doble (lado derecho)"
#: ui/help-overlay.ui:74
msgid "Previous item"
msgstr "Elemento anterior"
msgstr "Artículo anterior"
#: ui/help-overlay.ui:75
msgid "Double tap (left side)"
@@ -112,7 +112,7 @@ msgstr "Tocar doble (lado izquierdo)"
#: ui/help-overlay.ui:81
msgid "Change repeat mode"
msgstr "Cambiar a modo repetir"
msgstr "Cambiar a modo repetición"
#: ui/help-overlay.ui:87
msgid "Export to file"
@@ -132,7 +132,7 @@ msgstr "Pulsación larga | Clic derecho"
#: ui/help-overlay.ui:105
msgid "Seek forward"
msgstr "Buscar siguiente"
msgstr "Buscar adelante "
#: ui/help-overlay.ui:106
msgid "Swipe right | Scroll right"
@@ -140,7 +140,7 @@ msgstr "Deslizar a derecha | Desplazar a derecha"
#: ui/help-overlay.ui:112
msgid "Seek backward"
msgstr "Buscar anterior"
msgstr "Buscar a atrás"
#: ui/help-overlay.ui:113
msgid "Swipe left | Scroll left"
@@ -168,11 +168,11 @@ msgstr "Fijar a mudo"
#: ui/help-overlay.ui:139
msgid "Next chapter"
msgstr "Capítulo siguiente"
msgstr "Siguiente capítulo"
#: ui/help-overlay.ui:145
msgid "Previous chapter"
msgstr "Capítulo anterior"
msgstr "Anterior capítulo"
#: ui/preferences-plugin-ranking-subpage.ui:11
msgid "Decoders"
@@ -184,7 +184,7 @@ msgstr "Regresar a preferencias"
#: ui/preferences-window.ui:16
msgid "Behavior"
msgstr "Configuraciones"
msgstr "Comportamiento"
#: ui/preferences-window.ui:19
msgid "Auto fullscreen"
@@ -192,7 +192,7 @@ msgstr "Pantalla completa automática"
#: ui/preferences-window.ui:20
msgid "Enter fullscreen when playlist is replaced except floating mode"
msgstr "Entra a pantalla completa cuando se reemplaza la lista de reproducción, excepto en modo flotante"
msgstr "Fijar a pantalla completa cuando se reemplaza la lista de reproducción, excepto el modo flotante"
#: ui/preferences-window.ui:26
msgid "Ask to resume recent media"
@@ -216,7 +216,7 @@ msgstr "Nada por hacer"
#: ui/preferences-window.ui:45
msgid "Freeze last frame"
msgstr "Detener último fotograma"
msgstr "Congelar el último fotograma"
#: ui/preferences-window.ui:46
msgid "Close the app"
@@ -232,7 +232,7 @@ msgstr "Valor inicial personalizado"
#: ui/preferences-window.ui:60
msgid "Set custom volume at startup instead of restoring it"
msgstr "Establece un volumen personalizado al inicio en lugar de restaurarlo"
msgstr "Establecer un volumen personalizado al inicio en lugar de restaurarlo"
#: ui/preferences-window.ui:64
msgid "Volume percentage"
@@ -292,7 +292,7 @@ msgstr "Subtítulos"
#: ui/preferences-window.ui:144
msgid "Default font"
msgstr "Fuente por defecto"
msgstr "Fuente predeterminada"
#: ui/preferences-window.ui:154
msgid "Network"
@@ -356,7 +356,7 @@ msgstr "Renderizar sombras de ventana"
#: ui/preferences-window.ui:242
msgid "Disable to increase performance when windowed"
msgstr "Deshabilitado aumenta el rendimiento cuando se abre en ventana"
msgstr "Desactivar para aumentar el rendimiento cuando se abre en ventana"
#: ui/preferences-window.ui:253
msgid "Plugin ranking"
@@ -364,7 +364,7 @@ msgstr "Rango de enchufes"
#: ui/preferences-window.ui:254
msgid "Alter default ranks of GStreamer plugins"
msgstr "Altera los rangos predeterminados de los enchufes de GStreamer"
msgstr "Alterar los rangos predeterminados de los enchufes de GStreamer"
#: ui/preferences-window.ui:259
msgid "Use playbin3"
@@ -380,7 +380,7 @@ msgstr "Experimental"
#: ui/preferences-window.ui:268
msgid "Use PipeWire for audio output"
msgstr "Usar PipeWire"
msgstr "Usar PipeWire para la salida de audio"
#: src/buttons.js:201
#, javascript-format
@@ -389,7 +389,7 @@ msgstr "Decodificador: %s"
#: src/dialogs.js:152
msgid "Enter or drop URI here"
msgstr "Introducir la URI"
msgstr "Intoducir la URI aquí"
#: src/dialogs.js:157
msgid "Cancel"
@@ -455,5 +455,5 @@ msgstr "Canales"
#: src/widget.js:261
msgid "Disabled"
msgstr "Deshabilitar"
msgstr "Deshabilitado"

459
po/eu.po
View File

@@ -1,459 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-16 16:58\n"
"Last-Translator: \n"
"Language-Team: Basque\n"
"Language: eu_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: clapper\n"
"X-Crowdin-Project-ID: 473374\n"
"X-Crowdin-Language: eu\n"
"X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n"
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files…"
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI…"
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"
msgstr ""
#: ui/clapper.ui:20
msgid "Shortcuts"
msgstr ""
#: ui/clapper.ui:26
msgid "About Clapper"
msgstr ""
#: ui/elapsed-time-button.ui:27
msgid "Speed"
msgstr ""
#: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:83
#: ui/preferences-window.ui:215
msgid "Normal"
msgstr ""
#: ui/help-overlay.ui:10 ui/preferences-window.ui:12
msgid "General"
msgstr ""
#: ui/help-overlay.ui:13
msgid "Show shortcuts"
msgstr ""
#: ui/help-overlay.ui:19
msgid "Toggle fullscreen"
msgstr ""
#: ui/help-overlay.ui:20
msgid "Double tap | Double click"
msgstr ""
#: ui/help-overlay.ui:26
msgid "Leave fullscreen"
msgstr ""
#: ui/help-overlay.ui:32
msgid "Reveal OSD (fullscreen only)"
msgstr ""
#: ui/help-overlay.ui:33
msgid "Tap"
msgstr ""
#: ui/help-overlay.ui:39
msgid "Quit"
msgstr ""
#: ui/help-overlay.ui:47
msgid "Media"
msgstr ""
#: ui/help-overlay.ui:50
msgid "Open files"
msgstr ""
#: ui/help-overlay.ui:56 src/dialogs.js:137
msgid "Open URI"
msgstr ""
#: ui/help-overlay.ui:64
msgid "Playlist"
msgstr ""
#: ui/help-overlay.ui:67
msgid "Next item"
msgstr ""
#: ui/help-overlay.ui:68
msgid "Double tap (right side)"
msgstr ""
#: ui/help-overlay.ui:74
msgid "Previous item"
msgstr ""
#: ui/help-overlay.ui:75
msgid "Double tap (left side)"
msgstr ""
#: ui/help-overlay.ui:81
msgid "Change repeat mode"
msgstr ""
#: ui/help-overlay.ui:87
msgid "Export to file"
msgstr ""
#: ui/help-overlay.ui:95 ui/preferences-window.ui:119
msgid "Playback"
msgstr ""
#: ui/help-overlay.ui:98
msgid "Toggle play"
msgstr ""
#: ui/help-overlay.ui:99
msgid "Long press | Right click"
msgstr ""
#: ui/help-overlay.ui:105
msgid "Seek forward"
msgstr ""
#: ui/help-overlay.ui:106
msgid "Swipe right | Scroll right"
msgstr ""
#: ui/help-overlay.ui:112
msgid "Seek backward"
msgstr ""
#: ui/help-overlay.ui:113
msgid "Swipe left | Scroll left"
msgstr ""
#: ui/help-overlay.ui:119
msgid "Volume up"
msgstr ""
#: ui/help-overlay.ui:120
msgid "Swipe up | Scroll up"
msgstr ""
#: ui/help-overlay.ui:126
msgid "Volume down"
msgstr ""
#: ui/help-overlay.ui:127
msgid "Swipe down | Scroll down"
msgstr ""
#: ui/help-overlay.ui:133
msgid "Toggle mute"
msgstr ""
#: ui/help-overlay.ui:139
msgid "Next chapter"
msgstr ""
#: ui/help-overlay.ui:145
msgid "Previous chapter"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:11
msgid "Decoders"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:18
msgid "Return to the preferences"
msgstr ""
#: ui/preferences-window.ui:16
msgid "Behavior"
msgstr ""
#: ui/preferences-window.ui:19
msgid "Auto fullscreen"
msgstr ""
#: ui/preferences-window.ui:20
msgid "Enter fullscreen when playlist is replaced except floating mode"
msgstr ""
#: ui/preferences-window.ui:26
msgid "Ask to resume recent media"
msgstr ""
#: ui/preferences-window.ui:32
msgid "Float on all workspaces"
msgstr ""
#: ui/preferences-window.ui:33
msgid "This option only works on GNOME"
msgstr ""
#: ui/preferences-window.ui:39
msgid "After playback"
msgstr ""
#: ui/preferences-window.ui:44
msgid "Do nothing"
msgstr ""
#: ui/preferences-window.ui:45
msgid "Freeze last frame"
msgstr ""
#: ui/preferences-window.ui:46
msgid "Close the app"
msgstr ""
#: ui/preferences-window.ui:56
msgid "Volume"
msgstr ""
#: ui/preferences-window.ui:59
msgid "Custom initial value"
msgstr ""
#: ui/preferences-window.ui:60
msgid "Set custom volume at startup instead of restoring it"
msgstr ""
#: ui/preferences-window.ui:64
msgid "Volume percentage"
msgstr ""
#: ui/preferences-window.ui:75
msgid "Seeking"
msgstr ""
#: ui/preferences-window.ui:78
msgid "Mode"
msgstr ""
#: ui/preferences-window.ui:84
msgid "Accurate"
msgstr ""
#: ui/preferences-window.ui:85
msgid "Fast"
msgstr ""
#: ui/preferences-window.ui:93
msgid "Unit"
msgstr ""
#: ui/preferences-window.ui:98
msgid "Second"
msgstr ""
#: ui/preferences-window.ui:99
msgid "Minute"
msgstr ""
#: ui/preferences-window.ui:100
msgid "Percentage"
msgstr ""
#: ui/preferences-window.ui:108
msgid "Value"
msgstr ""
#: ui/preferences-window.ui:123
msgid "Audio"
msgstr ""
#: ui/preferences-window.ui:126
msgid "Offset in milliseconds"
msgstr ""
#: ui/preferences-window.ui:133
msgid "Only native audio formats"
msgstr ""
#: ui/preferences-window.ui:141
msgid "Subtitles"
msgstr ""
#: ui/preferences-window.ui:144
msgid "Default font"
msgstr ""
#: ui/preferences-window.ui:154
msgid "Network"
msgstr ""
#: ui/preferences-window.ui:158
msgid "Client"
msgstr ""
#: ui/preferences-window.ui:161
msgid "Progressive download buffering"
msgstr ""
#: ui/preferences-window.ui:169
msgid "Server"
msgstr ""
#: ui/preferences-window.ui:172
msgid "Control player remotely"
msgstr ""
#: ui/preferences-window.ui:176
msgid "Listening port"
msgstr ""
#: ui/preferences-window.ui:183
msgid "Run web application in background"
msgstr ""
#: ui/preferences-window.ui:184
msgid "Requires GTK compiled with Broadway backend"
msgstr ""
#: ui/preferences-window.ui:190
msgid "Web application port"
msgstr ""
#: ui/preferences-window.ui:204
msgid "Prefer adaptive streaming"
msgstr ""
#: ui/preferences-window.ui:210
msgid "Max quality"
msgstr ""
#: ui/preferences-window.ui:228
msgid "Tweaks"
msgstr ""
#: ui/preferences-window.ui:232
msgid "Appearance"
msgstr ""
#: ui/preferences-window.ui:235
msgid "Dark theme"
msgstr ""
#: ui/preferences-window.ui:241
msgid "Render window shadows"
msgstr ""
#: ui/preferences-window.ui:242
msgid "Disable to increase performance when windowed"
msgstr ""
#: ui/preferences-window.ui:253
msgid "Plugin ranking"
msgstr ""
#: ui/preferences-window.ui:254
msgid "Alter default ranks of GStreamer plugins"
msgstr ""
#: ui/preferences-window.ui:259
msgid "Use playbin3"
msgstr ""
#: ui/preferences-window.ui:260 ui/preferences-window.ui:269
msgid "Requires player restart"
msgstr ""
#: ui/preferences-window.ui:262 ui/preferences-window.ui:271
msgid "Experimental"
msgstr ""
#: ui/preferences-window.ui:268
msgid "Use PipeWire for audio output"
msgstr ""
#: src/buttons.js:201
#, javascript-format
msgid "Decoder: %s"
msgstr ""
#: src/dialogs.js:152
msgid "Enter or drop URI here"
msgstr ""
#: src/dialogs.js:157
msgid "Cancel"
msgstr ""
#: src/dialogs.js:158
msgid "Open"
msgstr ""
#: src/dialogs.js:226
msgid "Title"
msgstr ""
#: src/dialogs.js:227
msgid "Completed"
msgstr ""
#: src/dialogs.js:235
msgid "Resume playback?"
msgstr ""
#: src/dialogs.js:289
#, javascript-format
msgid "GTK version: %s"
msgstr ""
#: src/dialogs.js:290
#, javascript-format
msgid "Adwaita version: %s"
msgstr ""
#: src/dialogs.js:291
#, javascript-format
msgid "GStreamer version: %s"
msgstr ""
#: src/dialogs.js:292
#, javascript-format
msgid "GJS version: %s"
msgstr ""
#: src/dialogs.js:300
msgid "A GNOME media player powered by GStreamer"
msgstr ""
#. TRANSLATORS: Put your name(s) here for credits or leave untranslated
#: src/dialogs.js:305
msgid "translator-credits"
msgstr ""
#: src/revealers.js:170
#, javascript-format
msgid "Ends at: %s"
msgstr ""
#: src/widget.js:227 src/widget.js:236 src/widget.js:242 src/widget.js:248
msgid "Undetermined"
msgstr ""
#: src/widget.js:243
msgid "Channels"
msgstr ""
#: src/widget.js:261
msgid "Disabled"
msgstr ""

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

222
po/fr.po
View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-18 20:57\n"
"PO-Revision-Date: 2021-09-14 15:25\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -18,442 +18,442 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Ouvrir un fichier…"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Ouvrir une URL…"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"
msgstr "Préférences"
msgstr ""
#: ui/clapper.ui:20
msgid "Shortcuts"
msgstr "Raccourcis clavier"
msgstr ""
#: ui/clapper.ui:26
msgid "About Clapper"
msgstr "Á propos de Clapper"
msgstr ""
#: ui/elapsed-time-button.ui:27
msgid "Speed"
msgstr "Vitesse"
msgstr ""
#: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:83
#: ui/preferences-window.ui:215
msgid "Normal"
msgstr "Normal"
msgstr ""
#: ui/help-overlay.ui:10 ui/preferences-window.ui:12
msgid "General"
msgstr "Général"
msgstr ""
#: ui/help-overlay.ui:13
msgid "Show shortcuts"
msgstr "Montrer les raccourcis"
msgstr ""
#: ui/help-overlay.ui:19
msgid "Toggle fullscreen"
msgstr "Basculer en plein écran"
msgstr ""
#: ui/help-overlay.ui:20
msgid "Double tap | Double click"
msgstr "Tapoter/cliquer deux fois"
msgstr ""
#: ui/help-overlay.ui:26
msgid "Leave fullscreen"
msgstr "Sortir du plein écran"
msgstr ""
#: ui/help-overlay.ui:32
msgid "Reveal OSD (fullscreen only)"
msgstr "Afficher les commandes de lecture (seulement en plein écran)"
msgstr ""
#: ui/help-overlay.ui:33
msgid "Tap"
msgstr "Taper"
msgstr ""
#: ui/help-overlay.ui:39
msgid "Quit"
msgstr "Quitter"
msgstr ""
#: ui/help-overlay.ui:47
msgid "Media"
msgstr "Média"
msgstr ""
#: ui/help-overlay.ui:50
msgid "Open files"
msgstr "Ouvrir un fichier"
msgstr ""
#: ui/help-overlay.ui:56 src/dialogs.js:137
msgid "Open URI"
msgstr "Ouvrir une URL"
msgstr ""
#: ui/help-overlay.ui:64
msgid "Playlist"
msgstr "Playlist"
msgstr ""
#: ui/help-overlay.ui:67
msgid "Next item"
msgstr "Prochain média"
msgstr ""
#: ui/help-overlay.ui:68
msgid "Double tap (right side)"
msgstr "Tapoter deux fois (côté droit)"
msgstr ""
#: ui/help-overlay.ui:74
msgid "Previous item"
msgstr "Média précédent"
msgstr ""
#: ui/help-overlay.ui:75
msgid "Double tap (left side)"
msgstr "Tapoter deux fois (côté gauche)"
msgstr ""
#: ui/help-overlay.ui:81
msgid "Change repeat mode"
msgstr "Changer le mode de répétition"
msgstr ""
#: ui/help-overlay.ui:87
msgid "Export to file"
msgstr "Exporter vers un fichier"
msgstr ""
#: ui/help-overlay.ui:95 ui/preferences-window.ui:119
msgid "Playback"
msgstr "Lecture"
msgstr ""
#: ui/help-overlay.ui:98
msgid "Toggle play"
msgstr "Relancer/stopper la lecture"
msgstr ""
#: ui/help-overlay.ui:99
msgid "Long press | Right click"
msgstr "Longue pression | Clic droit"
msgstr ""
#: ui/help-overlay.ui:105
msgid "Seek forward"
msgstr "Avancer dans la lecture"
msgstr ""
#: ui/help-overlay.ui:106
msgid "Swipe right | Scroll right"
msgstr "Glisser/Faire défiler vers la droite"
msgstr ""
#: ui/help-overlay.ui:112
msgid "Seek backward"
msgstr "Reculer dans la lecture"
msgstr ""
#: ui/help-overlay.ui:113
msgid "Swipe left | Scroll left"
msgstr "Glisser/Faire défiler vers la gauche"
msgstr ""
#: ui/help-overlay.ui:119
msgid "Volume up"
msgstr "Augmenter le volume"
msgstr ""
#: ui/help-overlay.ui:120
msgid "Swipe up | Scroll up"
msgstr "Glisser/Défiler vers le haut"
msgstr ""
#: ui/help-overlay.ui:126
msgid "Volume down"
msgstr "Baisser le volume"
msgstr ""
#: ui/help-overlay.ui:127
msgid "Swipe down | Scroll down"
msgstr "Glisser/Défiler vers le bas"
msgstr ""
#: ui/help-overlay.ui:133
msgid "Toggle mute"
msgstr "Basculer le mode silencieux"
msgstr ""
#: ui/help-overlay.ui:139
msgid "Next chapter"
msgstr "Prochain chapitre"
msgstr ""
#: ui/help-overlay.ui:145
msgid "Previous chapter"
msgstr "Chapitre précédent"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:11
msgid "Decoders"
msgstr "Décodeurs"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:18
msgid "Return to the preferences"
msgstr "Retourner aux préférences"
msgstr ""
#: ui/preferences-window.ui:16
msgid "Behavior"
msgstr "Comportement"
msgstr ""
#: ui/preferences-window.ui:19
msgid "Auto fullscreen"
msgstr "Lecture automatique en plein écran"
msgstr ""
#: ui/preferences-window.ui:20
msgid "Enter fullscreen when playlist is replaced except floating mode"
msgstr "Basculer en plein écran quand la playlisyt est remplacée sauf si le mode flottant est activé"
msgstr ""
#: ui/preferences-window.ui:26
msgid "Ask to resume recent media"
msgstr "Demander pour reprendre à la position des médias récents"
msgstr ""
#: ui/preferences-window.ui:32
msgid "Float on all workspaces"
msgstr "Flotter sur tous les bureaux virtuels"
msgstr ""
#: ui/preferences-window.ui:33
msgid "This option only works on GNOME"
msgstr "Cette option ne marche qu'avec GNOME"
msgstr ""
#: ui/preferences-window.ui:39
msgid "After playback"
msgstr "Après la lecture"
msgstr ""
#: ui/preferences-window.ui:44
msgid "Do nothing"
msgstr "Ne rien faire"
msgstr ""
#: ui/preferences-window.ui:45
msgid "Freeze last frame"
msgstr "Geler la dernière image"
msgstr ""
#: ui/preferences-window.ui:46
msgid "Close the app"
msgstr "Fermer l'application"
msgstr ""
#: ui/preferences-window.ui:56
msgid "Volume"
msgstr "Volume"
msgstr ""
#: ui/preferences-window.ui:59
msgid "Custom initial value"
msgstr "Valeur initiale personnalisée"
msgstr ""
#: ui/preferences-window.ui:60
msgid "Set custom volume at startup instead of restoring it"
msgstr "Régler une valeur personnalisée du volume au démarrage au lien de restorer la valeur précédente"
msgstr ""
#: ui/preferences-window.ui:64
msgid "Volume percentage"
msgstr "Pourcentage du volume"
msgstr ""
#: ui/preferences-window.ui:75
msgid "Seeking"
msgstr "Avancement"
msgstr ""
#: ui/preferences-window.ui:78
msgid "Mode"
msgstr "Comportement"
msgstr ""
#: ui/preferences-window.ui:84
msgid "Accurate"
msgstr "Précis"
msgstr ""
#: ui/preferences-window.ui:85
msgid "Fast"
msgstr "Rapide"
msgstr ""
#: ui/preferences-window.ui:93
msgid "Unit"
msgstr "Unité des sauts"
msgstr ""
#: ui/preferences-window.ui:98
msgid "Second"
msgstr "Seconde"
msgstr ""
#: ui/preferences-window.ui:99
msgid "Minute"
msgstr "Minute"
msgstr ""
#: ui/preferences-window.ui:100
msgid "Percentage"
msgstr "Pourcentage"
msgstr ""
#: ui/preferences-window.ui:108
msgid "Value"
msgstr "Longueur du saut"
msgstr ""
#: ui/preferences-window.ui:123
msgid "Audio"
msgstr "Audio"
msgstr ""
#: ui/preferences-window.ui:126
msgid "Offset in milliseconds"
msgstr "Décalage en millisecondes"
msgstr ""
#: ui/preferences-window.ui:133
msgid "Only native audio formats"
msgstr "Seulement des formats audios natifs"
msgstr ""
#: ui/preferences-window.ui:141
msgid "Subtitles"
msgstr "Sous-titres"
msgstr ""
#: ui/preferences-window.ui:144
msgid "Default font"
msgstr "Police par défaut"
msgstr ""
#: ui/preferences-window.ui:154
msgid "Network"
msgstr "Réseau"
msgstr ""
#: ui/preferences-window.ui:158
msgid "Client"
msgstr "Client"
msgstr ""
#: ui/preferences-window.ui:161
msgid "Progressive download buffering"
msgstr "Téléchargement progressif dans le tampon"
msgstr ""
#: ui/preferences-window.ui:169
msgid "Server"
msgstr "Serveur"
msgstr ""
#: ui/preferences-window.ui:172
msgid "Control player remotely"
msgstr "Controler le lecteur à distance"
msgstr ""
#: ui/preferences-window.ui:176
msgid "Listening port"
msgstr "Écouter sur le port"
msgstr ""
#: ui/preferences-window.ui:183
msgid "Run web application in background"
msgstr "Lancer l'application web en arrière plan"
msgstr ""
#: ui/preferences-window.ui:184
msgid "Requires GTK compiled with Broadway backend"
msgstr "Requiert GTK compilé avec l'interface Broadway"
msgstr ""
#: ui/preferences-window.ui:190
msgid "Web application port"
msgstr "Port de l'application web"
msgstr ""
#: ui/preferences-window.ui:204
msgid "Prefer adaptive streaming"
msgstr "Préférer le streaming adaptatif"
msgstr ""
#: ui/preferences-window.ui:210
msgid "Max quality"
msgstr "Qualité maximale"
msgstr ""
#: ui/preferences-window.ui:228
msgid "Tweaks"
msgstr "Réglages"
msgstr ""
#: ui/preferences-window.ui:232
msgid "Appearance"
msgstr "Apparence"
msgstr ""
#: ui/preferences-window.ui:235
msgid "Dark theme"
msgstr "Thème sombre"
msgstr ""
#: ui/preferences-window.ui:241
msgid "Render window shadows"
msgstr "Afficher les ombres de la fenêtre"
msgstr ""
#: ui/preferences-window.ui:242
msgid "Disable to increase performance when windowed"
msgstr "Désactiver pour améliorer les performances quand fenêtré"
msgstr ""
#: ui/preferences-window.ui:253
msgid "Plugin ranking"
msgstr "Liste des plugins"
msgstr ""
#: ui/preferences-window.ui:254
msgid "Alter default ranks of GStreamer plugins"
msgstr "Changer les options par défaut de plugins GStreamer"
msgstr ""
#: ui/preferences-window.ui:259
msgid "Use playbin3"
msgstr "Utiliser playbin3"
msgstr ""
#: ui/preferences-window.ui:260 ui/preferences-window.ui:269
msgid "Requires player restart"
msgstr "Requiert le redémarrage du lecteur"
msgstr ""
#: ui/preferences-window.ui:262 ui/preferences-window.ui:271
msgid "Experimental"
msgstr "Expérimental"
msgstr ""
#: ui/preferences-window.ui:268
msgid "Use PipeWire for audio output"
msgstr "Utiliser PipeWire pour la sortie audio"
msgstr ""
#: src/buttons.js:201
#, javascript-format
msgid "Decoder: %s"
msgstr "Décodeur: %s"
msgstr ""
#: src/dialogs.js:152
msgid "Enter or drop URI here"
msgstr "Entrer ou déposer une URL ici"
msgstr ""
#: src/dialogs.js:157
msgid "Cancel"
msgstr "Annuler"
msgstr ""
#: src/dialogs.js:158
msgid "Open"
msgstr "Ouvrir"
msgstr ""
#: src/dialogs.js:226
msgid "Title"
msgstr "Titre"
msgstr ""
#: src/dialogs.js:227
msgid "Completed"
msgstr "Terminé"
msgstr ""
#: src/dialogs.js:235
msgid "Resume playback?"
msgstr "Reprendre la lecture?"
msgstr ""
#: src/dialogs.js:289
#, javascript-format
msgid "GTK version: %s"
msgstr "Version de GTK: %s"
msgstr ""
#: src/dialogs.js:290
#, javascript-format
msgid "Adwaita version: %s"
msgstr "Version d'Adwaita: %s"
msgstr ""
#: src/dialogs.js:291
#, javascript-format
msgid "GStreamer version: %s"
msgstr "Version de GStreamer: %s"
msgstr ""
#: src/dialogs.js:292
#, javascript-format
msgid "GJS version: %s"
msgstr "Version de GJS: %s"
msgstr ""
#: src/dialogs.js:300
msgid "A GNOME media player powered by GStreamer"
msgstr "Un lecteur multimédia pour GNOME propulsé par GStreamer"
msgstr ""
#. TRANSLATORS: Put your name(s) here for credits or leave untranslated
#: src/dialogs.js:305
msgid "translator-credits"
msgstr "Robin Verdenal-Tallieux"
msgstr ""
#: src/revealers.js:170
#, javascript-format
msgid "Ends at: %s"
msgstr "Finit à: %s"
msgstr ""
#: src/widget.js:227 src/widget.js:236 src/widget.js:242 src/widget.js:248
msgid "Undetermined"
msgstr "Indéterminé"
msgstr ""
#: src/widget.js:243
msgid "Channels"
msgstr "Chaines"
msgstr ""
#: src/widget.js:261
msgid "Disabled"
msgstr "Désactivé"
msgstr ""

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Fájlok megnyitása"
msgid "Open Files..."
msgstr "Fájlok megnyitása..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "URI megnyitása"
msgid "Open URI..."
msgstr "URI megnyitása..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Apri i File"
msgid "Open Files..."
msgstr "Apri i File..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Apri URI"
msgid "Open URI..."
msgstr "Apri URI..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr "Bestanden openen…"
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr "URI openen…"
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Otwórz pliki"
msgid "Open Files..."
msgstr "Otwórz pliki..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Otwórz URI"
msgid "Open URI..."
msgstr "Otwórz URI..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2021-10-21 00:29\n"
"PO-Revision-Date: 2021-09-14 15:25\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -18,89 +18,89 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Abrir Arquivos…"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Abrir URI…"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"
msgstr "Preferências"
msgstr ""
#: ui/clapper.ui:20
msgid "Shortcuts"
msgstr "Atalhos"
msgstr ""
#: ui/clapper.ui:26
msgid "About Clapper"
msgstr "Sobre o Clapper"
msgstr ""
#: ui/elapsed-time-button.ui:27
msgid "Speed"
msgstr "Velocidade"
msgstr ""
#: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:83
#: ui/preferences-window.ui:215
msgid "Normal"
msgstr "Predefinido"
msgstr ""
#: ui/help-overlay.ui:10 ui/preferences-window.ui:12
msgid "General"
msgstr "Geral"
msgstr ""
#: ui/help-overlay.ui:13
msgid "Show shortcuts"
msgstr "Mostrar atalhos"
msgstr ""
#: ui/help-overlay.ui:19
msgid "Toggle fullscreen"
msgstr "Mudar modo de ecrã"
msgstr ""
#: ui/help-overlay.ui:20
msgid "Double tap | Double click"
msgstr "Toque duplo duplo Clique duplo"
msgstr ""
#: ui/help-overlay.ui:26
msgid "Leave fullscreen"
msgstr "Sair do modo de ecrã completo"
msgstr ""
#: ui/help-overlay.ui:32
msgid "Reveal OSD (fullscreen only)"
msgstr "Revelar OSD (apenas em tela cheia)"
msgstr ""
#: ui/help-overlay.ui:33
msgid "Tap"
msgstr "Tocar"
msgstr ""
#: ui/help-overlay.ui:39
msgid "Quit"
msgstr "Sair"
msgstr ""
#: ui/help-overlay.ui:47
msgid "Media"
msgstr "Multimédia"
msgstr ""
#: ui/help-overlay.ui:50
msgid "Open files"
msgstr "Abrir ficheiro"
msgstr ""
#: ui/help-overlay.ui:56 src/dialogs.js:137
msgid "Open URI"
msgstr "Abrir URI"
msgstr ""
#: ui/help-overlay.ui:64
msgid "Playlist"
msgstr "Lista de reprodução"
msgstr ""
#: ui/help-overlay.ui:67
msgid "Next item"
msgstr "Próximo item"
msgstr ""
#: ui/help-overlay.ui:68
msgid "Double tap (right side)"
msgstr "Toque duplo (lado direito)"
msgstr ""
#: ui/help-overlay.ui:74
msgid "Previous item"

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr "Abrir Arquivos"
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr "Abrir URI"
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-16 14:15\n"
"PO-Revision-Date: 2021-09-17 08:56\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -18,12 +18,12 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Открыть файлы"
msgid "Open Files..."
msgstr "Открыть файлы ..."
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Открыть URI"
msgid "Open URI..."
msgstr "Открыть URI..."
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

222
po/sv.po
View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-16 14:15\n"
"PO-Revision-Date: 2021-09-14 15:24\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -18,442 +18,442 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgstr "Öppna filer…"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgstr "Öppna URL…"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4
msgid "Preferences"
msgstr "Inställningar"
msgstr ""
#: ui/clapper.ui:20
msgid "Shortcuts"
msgstr "Tangentbordsgenvägar"
msgstr ""
#: ui/clapper.ui:26
msgid "About Clapper"
msgstr "Om Clapper"
msgstr ""
#: ui/elapsed-time-button.ui:27
msgid "Speed"
msgstr "Hastighet"
msgstr ""
#: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:83
#: ui/preferences-window.ui:215
msgid "Normal"
msgstr "Normal"
msgstr ""
#: ui/help-overlay.ui:10 ui/preferences-window.ui:12
msgid "General"
msgstr "Allmänt"
msgstr ""
#: ui/help-overlay.ui:13
msgid "Show shortcuts"
msgstr "Visa tangentbordsgenvägar"
msgstr ""
#: ui/help-overlay.ui:19
msgid "Toggle fullscreen"
msgstr "Växla helskärmsläge"
msgstr ""
#: ui/help-overlay.ui:20
msgid "Double tap | Double click"
msgstr "Dubbeltryck | Dubbelklicka"
msgstr ""
#: ui/help-overlay.ui:26
msgid "Leave fullscreen"
msgstr "Lämna helskärmsläge"
msgstr ""
#: ui/help-overlay.ui:32
msgid "Reveal OSD (fullscreen only)"
msgstr "Visa OSD (endast helskärmsläge)"
msgstr ""
#: ui/help-overlay.ui:33
msgid "Tap"
msgstr "Tryck"
msgstr ""
#: ui/help-overlay.ui:39
msgid "Quit"
msgstr "Avsluta"
msgstr ""
#: ui/help-overlay.ui:47
msgid "Media"
msgstr "Media"
msgstr ""
#: ui/help-overlay.ui:50
msgid "Open files"
msgstr "Öppna filer"
msgstr ""
#: ui/help-overlay.ui:56 src/dialogs.js:137
msgid "Open URI"
msgstr "Öppna URI"
msgstr ""
#: ui/help-overlay.ui:64
msgid "Playlist"
msgstr "Spellista"
msgstr ""
#: ui/help-overlay.ui:67
msgid "Next item"
msgstr "Nästa föremål"
msgstr ""
#: ui/help-overlay.ui:68
msgid "Double tap (right side)"
msgstr "Dubbeltryck (höger sida)"
msgstr ""
#: ui/help-overlay.ui:74
msgid "Previous item"
msgstr "Föregående föremål"
msgstr ""
#: ui/help-overlay.ui:75
msgid "Double tap (left side)"
msgstr "Dubbeltryck (vänster sida)"
msgstr ""
#: ui/help-overlay.ui:81
msgid "Change repeat mode"
msgstr "Ändra upprepningsläge"
msgstr ""
#: ui/help-overlay.ui:87
msgid "Export to file"
msgstr "Exportera till fil"
msgstr ""
#: ui/help-overlay.ui:95 ui/preferences-window.ui:119
msgid "Playback"
msgstr "Uppspelning"
msgstr ""
#: ui/help-overlay.ui:98
msgid "Toggle play"
msgstr "Spela/pausa"
msgstr ""
#: ui/help-overlay.ui:99
msgid "Long press | Right click"
msgstr "Långtryck | Högerklicka"
msgstr ""
#: ui/help-overlay.ui:105
msgid "Seek forward"
msgstr "Spola framåt"
msgstr ""
#: ui/help-overlay.ui:106
msgid "Swipe right | Scroll right"
msgstr "Svep höger | Skrolla höger"
msgstr ""
#: ui/help-overlay.ui:112
msgid "Seek backward"
msgstr "Spola bakåt"
msgstr ""
#: ui/help-overlay.ui:113
msgid "Swipe left | Scroll left"
msgstr "Svep vänster | Skrolla vänster"
msgstr ""
#: ui/help-overlay.ui:119
msgid "Volume up"
msgstr "Höj volymen"
msgstr ""
#: ui/help-overlay.ui:120
msgid "Swipe up | Scroll up"
msgstr "Svep uppåt | Skrolla uppåt"
msgstr ""
#: ui/help-overlay.ui:126
msgid "Volume down"
msgstr "Sänk volymen"
msgstr ""
#: ui/help-overlay.ui:127
msgid "Swipe down | Scroll down"
msgstr "Svep nedåt | Skrolla nedåt"
msgstr ""
#: ui/help-overlay.ui:133
msgid "Toggle mute"
msgstr "Växla ljudet på/av"
msgstr ""
#: ui/help-overlay.ui:139
msgid "Next chapter"
msgstr "Nästa kapitel"
msgstr ""
#: ui/help-overlay.ui:145
msgid "Previous chapter"
msgstr "Föregående kapitel"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:11
msgid "Decoders"
msgstr "Avkodare"
msgstr ""
#: ui/preferences-plugin-ranking-subpage.ui:18
msgid "Return to the preferences"
msgstr "Återgå till inställningarna"
msgstr ""
#: ui/preferences-window.ui:16
msgid "Behavior"
msgstr "Beteende"
msgstr ""
#: ui/preferences-window.ui:19
msgid "Auto fullscreen"
msgstr "Automatiskt helskärmsläge"
msgstr ""
#: ui/preferences-window.ui:20
msgid "Enter fullscreen when playlist is replaced except floating mode"
msgstr "Växla till fullskärmsläge när en spellista ersätts, förutom i flytande läge"
msgstr ""
#: ui/preferences-window.ui:26
msgid "Ask to resume recent media"
msgstr "Be att återuppta senaste media"
msgstr ""
#: ui/preferences-window.ui:32
msgid "Float on all workspaces"
msgstr "Flyt på alla arbetsytor"
msgstr ""
#: ui/preferences-window.ui:33
msgid "This option only works on GNOME"
msgstr "Det här alternativet fungerar endast i GNOME"
msgstr ""
#: ui/preferences-window.ui:39
msgid "After playback"
msgstr "Efter uppspelning"
msgstr ""
#: ui/preferences-window.ui:44
msgid "Do nothing"
msgstr "Gör ingenting"
msgstr ""
#: ui/preferences-window.ui:45
msgid "Freeze last frame"
msgstr "Frys sista bildruta"
msgstr ""
#: ui/preferences-window.ui:46
msgid "Close the app"
msgstr "Stäng appen"
msgstr ""
#: ui/preferences-window.ui:56
msgid "Volume"
msgstr "Volym"
msgstr ""
#: ui/preferences-window.ui:59
msgid "Custom initial value"
msgstr "Anpassat startvärde"
msgstr ""
#: ui/preferences-window.ui:60
msgid "Set custom volume at startup instead of restoring it"
msgstr "Ställ in anpassad volym vid start istället för att återställa den"
msgstr ""
#: ui/preferences-window.ui:64
msgid "Volume percentage"
msgstr "Volymprocent"
msgstr ""
#: ui/preferences-window.ui:75
msgid "Seeking"
msgstr "Spolning"
msgstr ""
#: ui/preferences-window.ui:78
msgid "Mode"
msgstr "Läge"
msgstr ""
#: ui/preferences-window.ui:84
msgid "Accurate"
msgstr "Riktig"
msgstr ""
#: ui/preferences-window.ui:85
msgid "Fast"
msgstr "Snabbt"
msgstr ""
#: ui/preferences-window.ui:93
msgid "Unit"
msgstr "Enhet"
msgstr ""
#: ui/preferences-window.ui:98
msgid "Second"
msgstr "Sekund"
msgstr ""
#: ui/preferences-window.ui:99
msgid "Minute"
msgstr "Minut"
msgstr ""
#: ui/preferences-window.ui:100
msgid "Percentage"
msgstr "Procent"
msgstr ""
#: ui/preferences-window.ui:108
msgid "Value"
msgstr "Värde"
msgstr ""
#: ui/preferences-window.ui:123
msgid "Audio"
msgstr "Ljud"
msgstr ""
#: ui/preferences-window.ui:126
msgid "Offset in milliseconds"
msgstr "Förskjutning i millisekunder"
msgstr ""
#: ui/preferences-window.ui:133
msgid "Only native audio formats"
msgstr "Endast inbyggda ljudformat"
msgstr ""
#: ui/preferences-window.ui:141
msgid "Subtitles"
msgstr "Undertexter"
msgstr ""
#: ui/preferences-window.ui:144
msgid "Default font"
msgstr "Standardteckensnitt"
msgstr ""
#: ui/preferences-window.ui:154
msgid "Network"
msgstr "Nätverk"
msgstr ""
#: ui/preferences-window.ui:158
msgid "Client"
msgstr "Klient"
msgstr ""
#: ui/preferences-window.ui:161
msgid "Progressive download buffering"
msgstr "Progressiv nedladdningsbuffert"
msgstr ""
#: ui/preferences-window.ui:169
msgid "Server"
msgstr "Server"
msgstr ""
#: ui/preferences-window.ui:172
msgid "Control player remotely"
msgstr "Fjärrstyra spelaren"
msgstr ""
#: ui/preferences-window.ui:176
msgid "Listening port"
msgstr "Lyssningsport"
msgstr ""
#: ui/preferences-window.ui:183
msgid "Run web application in background"
msgstr "Kör webbprogram i bakgrunden"
msgstr ""
#: ui/preferences-window.ui:184
msgid "Requires GTK compiled with Broadway backend"
msgstr "Kräver GTK kompilerad med Broadway backend"
msgstr ""
#: ui/preferences-window.ui:190
msgid "Web application port"
msgstr "Port för webbprogram"
msgstr ""
#: ui/preferences-window.ui:204
msgid "Prefer adaptive streaming"
msgstr "Föredra adaptiv streaming"
msgstr ""
#: ui/preferences-window.ui:210
msgid "Max quality"
msgstr "Max kvalitet"
msgstr ""
#: ui/preferences-window.ui:228
msgid "Tweaks"
msgstr "Tweaks"
msgstr ""
#: ui/preferences-window.ui:232
msgid "Appearance"
msgstr "Utseende"
msgstr ""
#: ui/preferences-window.ui:235
msgid "Dark theme"
msgstr "Mörkt Tema"
msgstr ""
#: ui/preferences-window.ui:241
msgid "Render window shadows"
msgstr "Rendera fönsterskuggor"
msgstr ""
#: ui/preferences-window.ui:242
msgid "Disable to increase performance when windowed"
msgstr "Inaktivera för att öka prestanda när fönsterläge är på"
msgstr ""
#: ui/preferences-window.ui:253
msgid "Plugin ranking"
msgstr "Rangordning av plugin"
msgstr ""
#: ui/preferences-window.ui:254
msgid "Alter default ranks of GStreamer plugins"
msgstr "Ändra standardrankningar för GStreamer-plugins"
msgstr ""
#: ui/preferences-window.ui:259
msgid "Use playbin3"
msgstr "Använd playbin3"
msgstr ""
#: ui/preferences-window.ui:260 ui/preferences-window.ui:269
msgid "Requires player restart"
msgstr "Kräver omstart av spelaren"
msgstr ""
#: ui/preferences-window.ui:262 ui/preferences-window.ui:271
msgid "Experimental"
msgstr "Exprimentalt"
msgstr ""
#: ui/preferences-window.ui:268
msgid "Use PipeWire for audio output"
msgstr "Använd PipeWire för Ljudutgång"
msgstr ""
#: src/buttons.js:201
#, javascript-format
msgid "Decoder: %s"
msgstr "Avkodare: %s"
msgstr ""
#: src/dialogs.js:152
msgid "Enter or drop URI here"
msgstr "Ange eller släpp URI här"
msgstr ""
#: src/dialogs.js:157
msgid "Cancel"
msgstr "Avbryt"
msgstr ""
#: src/dialogs.js:158
msgid "Open"
msgstr "Öppna"
msgstr ""
#: src/dialogs.js:226
msgid "Title"
msgstr "Titel"
msgstr ""
#: src/dialogs.js:227
msgid "Completed"
msgstr "Klar"
msgstr ""
#: src/dialogs.js:235
msgid "Resume playback?"
msgstr "Återuppta uppspelningen?"
msgstr ""
#: src/dialogs.js:289
#, javascript-format
msgid "GTK version: %s"
msgstr "GTK version: %s"
msgstr ""
#: src/dialogs.js:290
#, javascript-format
msgid "Adwaita version: %s"
msgstr "Adwaita version: %s"
msgstr ""
#: src/dialogs.js:291
#, javascript-format
msgid "GStreamer version: %s"
msgstr "GStreamer version: %s"
msgstr ""
#: src/dialogs.js:292
#, javascript-format
msgid "GJS version: %s"
msgstr "GJS version: %s"
msgstr ""
#: src/dialogs.js:300
msgid "A GNOME media player powered by GStreamer"
msgstr "En media spelare för GNOME som drivs av GStreamer"
msgstr ""
#. TRANSLATORS: Put your name(s) here for credits or leave untranslated
#: src/dialogs.js:305
msgid "translator-credits"
msgstr "SA ST (sastofficial)"
msgstr ""
#: src/revealers.js:170
#, javascript-format
msgid "Ends at: %s"
msgstr "Slutar vid: %s"
msgstr ""
#: src/widget.js:227 src/widget.js:236 src/widget.js:242 src/widget.js:248
msgid "Undetermined"
msgstr "Obestämd"
msgstr ""
#: src/widget.js:243
msgid "Channels"
msgstr "Kanaler"
msgstr ""
#: src/widget.js:261
msgid "Disabled"
msgstr "Avstängd"
msgstr ""

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: clapper\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 16:35+0200\n"
"PO-Revision-Date: 2022-01-16 14:15\n"
"PO-Revision-Date: 2021-09-24 14:11\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr "打开文件…"
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr "打开 URI…"
#: ui/clapper.ui:16 ui/preferences-window.ui:4
@@ -39,7 +39,7 @@ msgstr "关于 Claper"
#: ui/elapsed-time-button.ui:27
msgid "Speed"
msgstr "速度"
msgstr ""
#: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:83
#: ui/preferences-window.ui:215
@@ -48,7 +48,7 @@ msgstr "一般"
#: ui/help-overlay.ui:10 ui/preferences-window.ui:12
msgid "General"
msgstr "常规​​​​​"
msgstr ""
#: ui/help-overlay.ui:13
msgid "Show shortcuts"
@@ -112,7 +112,7 @@ msgstr ""
#: ui/help-overlay.ui:81
msgid "Change repeat mode"
msgstr "更改循环模式"
msgstr ""
#: ui/help-overlay.ui:87
msgid "Export to file"
@@ -128,7 +128,7 @@ msgstr ""
#: ui/help-overlay.ui:99
msgid "Long press | Right click"
msgstr "长按 | 右键点击"
msgstr ""
#: ui/help-overlay.ui:105
msgid "Seek forward"
@@ -148,19 +148,19 @@ msgstr ""
#: ui/help-overlay.ui:119
msgid "Volume up"
msgstr "提高音量"
msgstr ""
#: ui/help-overlay.ui:120
msgid "Swipe up | Scroll up"
msgstr "向上滑动 | 向上滚动"
msgstr ""
#: ui/help-overlay.ui:126
msgid "Volume down"
msgstr "降低音量"
msgstr ""
#: ui/help-overlay.ui:127
msgid "Swipe down | Scroll down"
msgstr "向下滑动 | 向下滚动"
msgstr ""
#: ui/help-overlay.ui:133
msgid "Toggle mute"
@@ -208,19 +208,19 @@ msgstr "此选项仅适用于 GNOME"
#: ui/preferences-window.ui:39
msgid "After playback"
msgstr "播放结束后"
msgstr ""
#: ui/preferences-window.ui:44
msgid "Do nothing"
msgstr "不执行任何操作"
msgstr ""
#: ui/preferences-window.ui:45
msgid "Freeze last frame"
msgstr "停留在最后一帧"
msgstr ""
#: ui/preferences-window.ui:46
msgid "Close the app"
msgstr "关闭应用程序"
msgstr ""
#: ui/preferences-window.ui:56
msgid "Volume"
@@ -240,7 +240,7 @@ msgstr "音量百分比"
#: ui/preferences-window.ui:75
msgid "Seeking"
msgstr "定位播放"
msgstr ""
#: ui/preferences-window.ui:78
msgid "Mode"
@@ -308,7 +308,7 @@ msgstr ""
#: ui/preferences-window.ui:169
msgid "Server"
msgstr "服务器"
msgstr ""
#: ui/preferences-window.ui:172
msgid "Control player remotely"
@@ -328,7 +328,7 @@ msgstr ""
#: ui/preferences-window.ui:190
msgid "Web application port"
msgstr "Web 应用程序端口"
msgstr ""
#: ui/preferences-window.ui:204
msgid "Prefer adaptive streaming"
@@ -443,7 +443,7 @@ msgstr ""
#: src/revealers.js:170
#, javascript-format
msgid "Ends at: %s"
msgstr "结束于:%s"
msgstr ""
#: src/widget.js:227 src/widget.js:236 src/widget.js:242 src/widget.js:248
msgid "Undetermined"
@@ -455,5 +455,5 @@ msgstr "声道"
#: src/widget.js:261
msgid "Disabled"
msgstr "禁用"
msgstr ""

View File

@@ -18,11 +18,11 @@ msgstr ""
"X-Crowdin-File-ID: 31\n"
#: ui/clapper.ui:6
msgid "Open Files"
msgid "Open Files..."
msgstr ""
#: ui/clapper.ui:10
msgid "Open URI"
msgid "Open URI..."
msgstr ""
#: ui/clapper.ui:16 ui/preferences-window.ui:4

View File

@@ -96,6 +96,13 @@ class ClapperAppBase extends Gtk.Application
if(accels)
this.set_accels_for_action(`app.${name}`, accels);
}
const gtkSettings = Gtk.Settings.get_default();
settings.bind(
'dark-theme', gtkSettings,
'gtk-application-prefer-dark-theme',
Gio.SettingsBindFlags.GET
);
this.doneFirstActivate = true;
}
});

25
src/appRemote.js Normal file
View File

@@ -0,0 +1,25 @@
const { GObject } = imports.gi;
const { AppBase } = imports.src.appBase;
const { HeaderBarRemote } = imports.src.headerbarRemote;
const { WidgetRemote } = imports.src.widgetRemote;
var AppRemote = GObject.registerClass({
GTypeName: 'ClapperAppRemote',
},
class ClapperAppRemote extends AppBase
{
vfunc_startup()
{
super.vfunc_startup();
const window = this.active_window;
const clapperWidget = new WidgetRemote();
window.set_child(clapperWidget);
const headerBar = new HeaderBarRemote();
window.set_titlebar(headerBar);
window.maximize();
}
});

View File

@@ -165,7 +165,7 @@ class ClapperElapsedTimeButton extends PopoverButtonBase
setInitialState()
{
this.label = '00000000';
this.label = '00:00/00:00';
}
setFullscreenMode(isFullscreen, isMobileMonitor)

6
src/controls.js vendored
View File

@@ -7,6 +7,8 @@ const Revealers = imports.src.revealers;
const { debug } = Debug;
const { settings } = Misc;
const INITIAL_ELAPSED = '00:00/00:00';
var Controls = GObject.registerClass({
GTypeName: 'ClapperControls',
},
@@ -27,7 +29,7 @@ class ClapperControls extends Gtk.Box
this.isMobile = false;
this.showHours = false;
this.durationFormatted = '0000';
this.durationFormatted = '00:00';
this.revealersArr = [];
this.chapters = null;
@@ -146,7 +148,7 @@ class ClapperControls extends Gtk.Box
value = value || 0;
const elapsed = Misc.getFormattedTime(value, this.showHours)
+ '' + this.durationFormatted;
+ '/' + this.durationFormatted;
this.elapsedButton.label = elapsed;
}

70
src/daemon.js Normal file
View File

@@ -0,0 +1,70 @@
const { Gio, GLib, GObject } = imports.gi;
const Debug = imports.src.debug;
const { debug } = Debug;
var Daemon = GObject.registerClass({
GTypeName: 'ClapperDaemon',
},
class ClapperDaemon extends Gio.SubprocessLauncher
{
_init()
{
const port = ARGV[0] || 8080;
/* FIXME: show output when debugging is on */
const flags = Gio.SubprocessFlags.STDOUT_SILENCE
| Gio.SubprocessFlags.STDERR_SILENCE;
super._init({ flags });
this.errMsg = 'exited with error or was forced to close';
this.loop = GLib.MainLoop.new(null, false);
this.broadwayd = this.spawnv(['gtk4-broadwayd', '--port=' + port]);
this.broadwayd.wait_async(null, this._onBroadwaydClosed.bind(this));
this.setenv('GDK_BACKEND', 'broadway', true);
const remoteApp = this.spawnv(['com.github.rafostar.Clapper.Remote']);
remoteApp.wait_async(null, this._onRemoteClosed.bind(this));
this.loop.run();
}
_checkProcResult(proc, result)
{
let hadError = false;
try {
hadError = proc.wait_finish(result);
}
catch(err) {
debug(err);
}
return hadError;
}
_onBroadwaydClosed(proc, result)
{
const hadError = this._checkProcResult(proc, result);
if(hadError)
debug(`broadwayd ${this.errMsg}`);
this.broadwayd = null;
this.loop.quit();
}
_onRemoteClosed(proc, result)
{
const hadError = this._checkProcResult(proc, result);
if(hadError)
debug(`remote app ${this.errMsg}`);
if(this.broadwayd)
this.broadwayd.force_exit();
}
});

165
src/dash.js Normal file
View File

@@ -0,0 +1,165 @@
const Debug = imports.src.debug;
const FileOps = imports.src.fileOps;
const Misc = imports.src.misc;
const { debug } = Debug;
function generateDash(dashInfo)
{
debug('generating dash');
const bufferSec = Math.min(4, dashInfo.duration);
const dash = [
`<?xml version="1.0" encoding="UTF-8"?>`,
`<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xmlns="urn:mpeg:dash:schema:mpd:2011"`,
` xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"`,
` type="static"`,
` mediaPresentationDuration="PT${dashInfo.duration}S"`,
` minBufferTime="PT${bufferSec}S"`,
` profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">`,
` <Period>`
];
for(let adaptation of dashInfo.adaptations)
dash.push(_addAdaptationSet(adaptation));
dash.push(
` </Period>`,
`</MPD>`
);
debug('dash generated');
return dash.join('\n');
}
function _addAdaptationSet(streamsArr)
{
/* We just need it for adaptation type,
* so any stream will do */
const { mimeInfo } = streamsArr[0];
const adaptArr = [
`contentType="${mimeInfo.content}"`,
`mimeType="${mimeInfo.type}"`,
`subsegmentAlignment="true"`,
`subsegmentStartsWithSAP="1"`,
];
const widthArr = [];
const heightArr = [];
const fpsArr = [];
const representations = [];
for(let stream of streamsArr) {
/* No point parsing if no URL */
if(!stream.url)
continue;
if(stream.width && stream.height) {
widthArr.push(stream.width);
heightArr.push(stream.height);
}
if(stream.fps)
fpsArr.push(stream.fps);
representations.push(_getStreamRepresentation(stream));
}
if(widthArr.length && heightArr.length) {
const maxWidth = Math.max.apply(null, widthArr);
const maxHeight = Math.max.apply(null, heightArr);
const par = _getPar(maxWidth, maxHeight);
adaptArr.push(`maxWidth="${maxWidth}"`);
adaptArr.push(`maxHeight="${maxHeight}"`);
adaptArr.push(`par="${par}"`);
}
if(fpsArr.length) {
const maxFps = Math.max.apply(null, fpsArr);
adaptArr.push(`maxFrameRate="${maxFps}"`);
}
const adaptationSet = [
` <AdaptationSet ${adaptArr.join(' ')}>`,
representations.join('\n'),
` </AdaptationSet>`
];
return adaptationSet.join('\n');
}
function _getStreamRepresentation(stream)
{
const repOptsArr = [
`id="${stream.itag}"`,
`codecs="${stream.mimeInfo.codecs}"`,
`bandwidth="${stream.bitrate}"`,
];
if(stream.width && stream.height) {
repOptsArr.push(`width="${stream.width}"`);
repOptsArr.push(`height="${stream.height}"`);
repOptsArr.push(`sar="1:1"`);
}
if(stream.fps)
repOptsArr.push(`frameRate="${stream.fps}"`);
const repArr = [
` <Representation ${repOptsArr.join(' ')}>`,
];
if(stream.audioChannels) {
const audioConfArr = [
`schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"`,
`value="${stream.audioChannels}"`,
];
repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`);
}
repArr.push(
` <BaseURL>${stream.url}</BaseURL>`
);
if(stream.indexRange) {
const segRange = `${stream.indexRange.start}-${stream.indexRange.end}`;
repArr.push(
` <SegmentBase indexRange="${segRange}">`
);
if(stream.initRange) {
const initRange = `${stream.initRange.start}-${stream.initRange.end}`;
repArr.push(
` <Initialization range="${initRange}"/>`
);
}
repArr.push(
` </SegmentBase>`
);
}
repArr.push(
` </Representation>`
);
return repArr.join('\n');
}
function _getPar(width, height)
{
const gcd = _getGCD(width, height);
width /= gcd;
height /= gcd;
return `${width}:${height}`;
}
function _getGCD(width, height)
{
return (height)
? _getGCD(height, width % height)
: width;
}

View File

@@ -14,11 +14,13 @@ const clapperDebugger = new Debug.Debugger('Clapper', {
}),
high_precision: true,
});
clapperDebugger.enabled = (
var enabled = (
clapperDebugger.enabled
|| G_DEBUG_ENV != null
&& G_DEBUG_ENV.includes('Clapper')
);
clapperDebugger.enabled = enabled;
function _logStructured(debuggerName, msg, level)
{
@@ -32,15 +34,11 @@ function _logStructured(debuggerName, msg, level)
function _debug(debuggerName, msg)
{
if(msg.message) {
_logStructured(
debuggerName,
msg.message,
_logStructured(debuggerName, msg.message,
GLib.LogLevelFlags.LEVEL_CRITICAL
);
return;
}
clapperDebugger.debug(msg);
}
@@ -53,3 +51,8 @@ function warn(msg)
{
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_WARNING);
}
function message(msg)
{
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_MESSAGE);
}

297
src/gtuber.js Normal file
View File

@@ -0,0 +1,297 @@
const { Gio, GstClapper } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const FileOps = imports.src.fileOps;
const Gtuber = Misc.tryImport('Gtuber');
const { debug, warn } = Debug;
const { settings } = Misc;
const best = {
video: null,
audio: null,
video_audio: null,
};
const codecPairs = [];
const qualityType = {
0: 30, // normal
1: 60, // hfr
};
var isAvailable = (Gtuber != null);
var cancellable = null;
let client = null;
function resetBestStreams()
{
best.video = null;
best.audio = null;
best.video_audio = null;
}
function isStreamAllowed(stream, opts)
{
const vcodec = stream.video_codec;
const acodec = stream.audio_codec;
if(
vcodec
&& (!vcodec.startsWith(opts.vcodec)
|| (stream.height < 240 || stream.height > opts.height)
|| stream.fps > qualityType[opts.quality])
) {
return false;
}
if(
acodec
&& (!acodec.startsWith(opts.acodec))
) {
return false;
}
return (vcodec != null || acodec != null);
}
function updateBestStreams(streams, opts)
{
for(let stream of streams) {
if(!isStreamAllowed(stream, opts))
continue;
const type = (stream.video_codec && stream.audio_codec)
? 'video_audio'
: (stream.video_codec)
? 'video'
: 'audio';
if(!best[type] || best[type].bitrate < stream.bitrate)
best[type] = stream;
}
}
function _streamFilter(opts, stream)
{
switch(stream) {
case best.video:
return (best.audio != null || best.video_audio == null);
case best.audio:
return (best.video != null || best.video_audio == null);
case best.video_audio:
return (best.video == null || best.audio == null);
default:
return (opts.adaptive)
? isStreamAllowed(stream, opts)
: false;
}
}
function generateManifest(info, opts)
{
const gen = new Gtuber.ManifestGenerator({
pretty: Debug.enabled,
});
gen.set_media_info(info);
gen.set_filter_func(_streamFilter.bind(this, opts));
debug('trying to get manifest');
for(let pair of codecPairs) {
opts.vcodec = pair[0];
opts.acodec = pair[1];
/* Find best streams among adaptive ones */
if (!opts.adaptive)
updateBestStreams(info.get_adaptive_streams(), opts);
const data = gen.to_data();
/* Release our ref */
if (!opts.adaptive)
resetBestStreams();
if(data) {
debug('got manifest');
return data;
}
}
debug('manifest not generated');
return null;
}
function getBestCombinedUri(info, opts)
{
const streams = info.get_streams();
debug('searching for best combined URI');
for(let pair of codecPairs) {
opts.vcodec = pair[0];
opts.acodec = pair[1];
/* Find best non-adaptive stream */
updateBestStreams(streams, opts);
const bestUri = (best.video_audio)
? best.video_audio.get_uri()
: (best.audio)
? best.audio.get_uri()
: (best.video)
? best.video.get_uri()
: null;
/* Release our ref */
resetBestStreams();
if(bestUri) {
debug('got best possible URI');
return bestUri;
}
}
/* If still nothing find stream by height */
for(let stream of streams) {
const height = stream.get_height();
if(!height || height > opts.height)
continue;
if(!best.video_audio || best.video_audio.height < stream.height)
best.video_audio = stream;
}
const anyUri = (best.video_audio)
? best.video_audio.get_uri()
: null;
/* Release our ref */
resetBestStreams();
if (anyUri)
debug('got any URI');
return anyUri;
}
async function _parseMediaInfoAsync(info, player)
{
const resp = {
uri: null,
title: info.title,
};
const { root } = player.widget;
const surface = root.get_surface();
const monitor = root.display.get_monitor_at_surface(surface);
const opts = {
width: monitor.geometry.width * monitor.scale_factor,
height: monitor.geometry.height * monitor.scale_factor,
vcodec: null,
acodec: null,
quality: settings.get_int('yt-quality-type'),
adaptive: settings.get_boolean('yt-adaptive-enabled'),
};
if(info.has_adaptive_streams) {
const data = generateManifest(info, opts);
if(data) {
const manifestFile = await FileOps.saveFilePromise(
'tmp', null, 'manifest', data
).catch(debug);
if(!manifestFile)
throw new Error('Gtuber: no manifest file was generated');
resp.uri = manifestFile.get_uri();
return resp;
}
}
resp.uri = getBestCombinedUri(info, opts);
if(!resp.uri)
throw new Error("Gtuber: no compatible stream found");
return resp;
}
function _createClient(player)
{
client = new Gtuber.Client();
debug('created new gtuber client');
/* TODO: config based on what HW supports */
//codecPairs.push(['vp9', 'opus']);
codecPairs.push(['avc', 'mp4a']);
}
function mightHandleUri(uri)
{
const unsupported = [
'file', 'fd', 'dvd', 'cdda',
'dvb', 'v4l2', 'gs'
];
return !unsupported.includes(Misc.getUriProtocol(uri));
}
function cancelFetching()
{
if(cancellable && !cancellable.is_cancelled())
cancellable.cancel();
}
function parseUriPromise(uri, player)
{
return new Promise((resolve, reject) => {
if(!client) {
if(!isAvailable) {
debug('gtuber is not installed');
return resolve({ uri, title: null });
}
_createClient(player);
}
/* Stop to show reaction and restore internet bandwidth */
if(player.state !== GstClapper.ClapperState.STOPPED)
player.stop();
cancellable = new Gio.Cancellable();
debug('gtuber is fetching media info...');
client.fetch_media_info_async(uri, cancellable, (client, task) => {
cancellable = null;
let info = null;
try {
info = client.fetch_media_info_finish(task);
debug('gtuber successfully fetched media info');
}
catch(err) {
const taskCancellable = task.get_cancellable();
if(taskCancellable.is_cancelled())
return reject(err);
const gtuberNoPlugin = (
err.domain === Gtuber.ClientError.quark()
&& err.code === Gtuber.ClientError.NO_PLUGIN
);
if(!gtuberNoPlugin)
return reject(err);
warn(`Gtuber: ${err.message}, trying URI as is...`);
/* Allow handling URI as is via GStreamer plugins */
return resolve({ uri, title: null });
}
_parseMediaInfoAsync(info, player)
.then(resp => resolve(resp))
.catch(err => reject(err));
});
});
}

22
src/headerbarRemote.js Normal file
View File

@@ -0,0 +1,22 @@
const { GObject } = imports.gi;
const { HeaderBarBase } = imports.src.headerbarBase;
var HeaderBarRemote = GObject.registerClass({
GTypeName: 'ClapperHeaderBarRemote',
},
class ClapperHeaderBarRemote extends HeaderBarBase
{
_init()
{
super._init();
this.extraButtonsBox.visible = false;
}
_onWindowButtonActivate(action)
{
if(action === 'toggle-maximized')
action = 'toggle_maximized';
this.root.child.sendWs(action);
}
});

View File

@@ -1,6 +1,7 @@
imports.gi.versions.Gdk = '4.0';
imports.gi.versions.Gtk = '4.0';
imports.gi.versions.Soup = '3.0';
imports.gi.versions.Soup = '2.4';
imports.gi.versions.Gtuber = '0.0';
pkg.initGettext();
pkg.initFormat();

6
src/mainDaemon.js Normal file
View File

@@ -0,0 +1,6 @@
const { Daemon } = imports.src.daemon;
function main()
{
new Daemon();
}

20
src/mainRemote.js Normal file
View File

@@ -0,0 +1,20 @@
imports.gi.versions.Gdk = '4.0';
imports.gi.versions.Gtk = '4.0';
imports.gi.versions.Soup = '2.4';
imports.gi.versions.Gtuber = '0.0';
pkg.initGettext();
const Misc = imports.src.misc;
Misc.appId += '.Remote';
const { Gtk, Adw } = imports.gi;
const { AppRemote } = imports.src.appRemote;
function main(argv)
{
Gtk.init();
Adw.init();
new AppRemote().run(argv);
}

View File

@@ -1,7 +1,8 @@
const { Gio, GLib, Gdk, Gtk } = imports.gi;
const Debug = imports.src.debug;
const { debug } = Debug;
const { debug, message } = Debug;
const failedImports = [];
var appName = 'Clapper';
var appId = 'com.github.rafostar.Clapper';
@@ -28,6 +29,23 @@ const subsKeys = Object.keys(subsTitles);
let inhibitCookie;
function tryImport(libName)
{
let lib = null;
try {
lib = imports.gi[libName];
}
catch(err) {
if(!failedImports.includes(libName)) {
failedImports.push(libName);
message(err.message);
}
}
return lib;
}
function getResourceUri(path)
{
const res = `file://${pkg.pkgdatadir}/${path}`;
@@ -161,8 +179,8 @@ function getFormattedTime(time, showHours)
time -= minutes * 60;
const seconds = ('0' + Math.floor(time)).slice(-2);
const parsed = (hours) ? `${hours}` : '';
return parsed + `${minutes}${seconds}`;
const parsed = (hours) ? `${hours}:` : '';
return parsed + `${minutes}:${seconds}`;
}
function parsePlaylistFiles(filesArray)

View File

@@ -1,8 +1,10 @@
const { Adw, Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const Gtuber = imports.src.gtuber;
const { PlaylistWidget } = imports.src.playlist;
const { WebApp } = imports.src.webApp;
const { debug, warn } = Debug;
const { settings } = Misc;
@@ -42,10 +44,12 @@ class ClapperPlayer extends GstClapper.Clapper
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.playlistWidget = new PlaylistWidget();
this.seekDone = true;
this.needsFastSeekRestore = false;
this.customVideoTitle = null;
this.windowMapped = false;
this.quitOnStop = false;
@@ -69,7 +73,6 @@ class ClapperPlayer extends GstClapper.Clapper
set_and_bind_settings()
{
const settingsToSet = [
'dark-theme',
'after-playback',
'seeking-mode',
'audio-offset',
@@ -137,19 +140,31 @@ class ClapperPlayer extends GstClapper.Clapper
set_uri(uri)
{
if(Misc.getUriProtocol(uri) === 'file') {
const file = Misc.getFileFromLocalUri(uri);
if(!file) {
if(!this.playlistWidget.nextTrack())
debug('set media reached end of playlist');
this.customVideoTitle = null;
Gtuber.cancelFetching();
return;
}
if(uri.endsWith('.claps')) {
this.load_playlist_file(file);
if(Gtuber.mightHandleUri(uri)) {
Gtuber.parseUriPromise(uri, this)
.then(res => {
this.customVideoTitle = res.title;
super.set_uri(res.uri);
})
.catch(debug);
return;
}
return;
}
const file = Misc.getFileFromLocalUri(uri);
if(!file) {
if(!this.playlistWidget.nextTrack())
debug('set media reached end of playlist');
return;
}
if(uri.endsWith('.claps')) {
this.load_playlist_file(file);
return;
}
super.set_uri(uri);
@@ -630,19 +645,6 @@ class ClapperPlayer extends GstClapper.Clapper
break;
}
break;
case 'dark-theme':
/* TODO: Remove libadwaita alpha2 compat someday */
if (Adw.StyleManager != null) {
const styleManager = Adw.StyleManager.get_default();
styleManager.color_scheme = (settings.get_boolean(key))
? Adw.ColorScheme.FORCE_DARK
: Adw.ColorScheme.FORCE_LIGHT;
}
else {
const gtkSettings = Gtk.Settings.get_default();
gtkSettings.gtk_application_prefer_dark_theme = settings.get_boolean(key);
}
break;
case 'render-shadows':
root = this.widget.get_root();
if(!root) break;
@@ -673,6 +675,7 @@ class ClapperPlayer extends GstClapper.Clapper
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
break;
case 'webserver-enabled':
case 'webapp-enabled':
const webserverEnabled = settings.get_boolean('webserver-enabled');
if(webserverEnabled) {
@@ -687,8 +690,22 @@ class ClapperPlayer extends GstClapper.Clapper
this.webserver.passMsgData = this.receiveWs.bind(this);
}
this.webserver.startListening();
const webappEnabled = settings.get_boolean('webapp-enabled');
if(!this.webapp && !webappEnabled)
break;
if(webappEnabled) {
if(!this.webapp)
this.webapp = new WebApp();
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
}
}
else if(this.webserver) {
/* remote app will close when connection is lost
* which will cause the daemon to close too */
this.webserver.stopListening();
}
break;

51
src/playerRemote.js Normal file
View File

@@ -0,0 +1,51 @@
const { GObject } = imports.gi;
const { WebClient } = imports.src.webClient;
var ClapperState = {
STOPPED: 0,
BUFFERING: 1,
PAUSED: 2,
PLAYING: 3,
};
var PlayerRemote = GObject.registerClass({
GTypeName: 'ClapperPlayerRemote',
},
class ClapperPlayerRemote extends GObject.Object
{
_init()
{
super._init();
this.webclient = new WebClient();
}
set_playlist(playlist)
{
const uris = [];
/* We can not send GioFiles via WebSocket */
for(let source of playlist)
uris.push(this._getSourceUri(source));
this.webclient.sendMessage({
action: 'set_playlist',
value: uris
});
}
set_subtitles(source)
{
this.webclient.sendMessage({
action: 'set_subtitles',
value: this._getSourceUri(source)
});
}
_getSourceUri(source)
{
return (source.get_uri != null)
? source.get_uri()
: source;
}
});

View File

@@ -1,6 +1,7 @@
const { Adw, GObject, Gio, Gst, Gtk } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const Gtuber = imports.src.gtuber;
const { debug } = Debug;
const { settings } = Misc;
@@ -440,15 +441,8 @@ class ClapperPrefsPluginExpander extends Adw.ExpanderRow
const featuresNames = Object.keys(pluginsData[this.title]);
debug(`Adding ${featuresNames.length} features to the list of plugin: ${this.title}`);
for(let featureObj of pluginsData[this.title]) {
const prefsPluginFeature = new PrefsPluginFeature(featureObj);
/* TODO: Remove old libadwaita compat */
if(this.add_row)
this.add_row(prefsPluginFeature);
else
this.add(prefsPluginFeature);
}
for(let featureObj of pluginsData[this.title])
this.add(new PrefsPluginFeature(featureObj));
}
});
@@ -544,6 +538,7 @@ class ClapperPrefsPluginRankingSubpage extends Gtk.Box
var PrefsWindow = GObject.registerClass({
GTypeName: 'ClapperPrefsWindow',
Template: Misc.getResourceUri('ui/preferences-window.ui'),
InternalChildren: ['gtuber_group'],
},
class ClapperPrefsWindow extends Adw.PreferencesWindow
{
@@ -553,11 +548,7 @@ class ClapperPrefsWindow extends Adw.PreferencesWindow
transient_for: window,
});
/* FIXME: old libadwaita compat, should be
* normally in prefs UI file */
this.can_swipe_back = true;
this.can_navigate_back = true;
this._gtuber_group.visible = Gtuber.isAvailable;
this.show();
}
});

View File

@@ -59,8 +59,8 @@ class ClapperRevealerTop extends CustomRevealer
const initTime = GLib.DateTime.new_now_local().format('%X');
this.timeFormat = (initTime.length > 8)
? '%I%M %p'
: '%H%M';
? '%I:%M %p'
: '%H:%M';
this.mediaTitle = new Gtk.Label({
ellipsize: Pango.EllipsizeMode.END,
@@ -321,8 +321,6 @@ class ClapperControlsRevealer extends Gtk.Revealer
const isStick = (isFloating && settings.get_boolean('floating-stick'));
DBus.shellWindowEval('stick', isStick);
this.root.child.refreshWindowTitle(this.root.title);
}
_onControlsRevealed()

51
src/webApp.js Normal file
View File

@@ -0,0 +1,51 @@
const { Gio, GObject } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const { debug } = Debug;
var WebApp = GObject.registerClass({
GTypeName: 'ClapperWebApp',
},
class ClapperWebApp extends Gio.SubprocessLauncher
{
_init()
{
const flags = Gio.SubprocessFlags.STDOUT_SILENCE
| Gio.SubprocessFlags.STDERR_SILENCE;
super._init({ flags });
this.daemonApp = null;
}
startDaemonApp(port)
{
if(this.daemonApp)
return;
this.daemonApp = this.spawnv([Misc.appId + '.Daemon', String(port)]);
this.daemonApp.wait_async(null, this._onDaemonClosed.bind(this));
debug('daemon app started');
}
_onDaemonClosed(proc, result)
{
let hadError;
try {
hadError = proc.wait_finish(result);
}
catch(err) {
debug(err);
}
this.daemonApp = null;
if(hadError)
debug('daemon app exited with error or was forced to close');
debug('daemon app closed');
}
});

90
src/webClient.js Normal file
View File

@@ -0,0 +1,90 @@
const { Gio, GObject, Soup } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const WebHelpers = imports.src.webHelpers;
const { debug } = Debug;
const { settings } = Misc;
var WebClient = GObject.registerClass({
GTypeName: 'ClapperWebClient',
},
class ClapperWebClient extends Soup.Session
{
_init(port)
{
super._init({
timeout: 3,
use_thread_context: true,
});
this.wsConn = null;
this.connectWebsocket();
}
connectWebsocket()
{
if(this.wsConn)
return;
const port = settings.get_int('webserver-port');
const message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`);
this.websocket_connect_async(message, null, null, null, this._onWsConnect.bind(this));
debug('connecting WebSocket to Clapper app');
}
sendMessage(data)
{
if(
!this.wsConn
|| this.wsConn.state !== Soup.WebsocketState.OPEN
)
return;
this.wsConn.send_text(JSON.stringify(data));
}
passMsgData(action, value)
{
}
_onWsConnect(session, result)
{
let connection = null;
try {
connection = this.websocket_connect_finish(result);
}
catch(err) {
debug(err);
}
if(!connection)
return this.passMsgData('close');
connection.connect('message', this._onWsMessage.bind(this));
connection.connect('closed', this._onWsClosed.bind(this));
this.wsConn = connection;
debug('successfully connected WebSocket');
}
_onWsMessage(connection, dataType, bytes)
{
const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes);
if(success)
this.passMsgData(parsedMsg.action, parsedMsg.value);
}
_onWsClosed(connection)
{
debug('closed WebSocket connection');
this.wsConn = null;
this.passMsgData('close');
}
});

View File

@@ -107,7 +107,7 @@ class ClapperWebServer extends Soup.Server
this.remove_handler('/');
}
_onWsConnection(server, msg, path, connection)
_onWsConnection(server, connection)
{
debug('new WebSocket connection');

View File

@@ -224,7 +224,7 @@ class ClapperWidget extends Gtk.Grid
case GstClapper.ClapperVideoInfo:
type = 'video';
codec = info.get_codec() || _('Undetermined');
text = `${codec}, ${info.get_width()}×${info.get_height()}`;
text = `${codec}, ${info.get_width()}x${info.get_height()}`;
let fps = info.get_framerate();
fps = Number((fps[0] / fps[1]).toFixed(2));
if(fps)
@@ -277,8 +277,7 @@ class ClapperWidget extends Gtk.Grid
if(currStream && type !== 'subtitle') {
const caps = currStream.get_caps();
if (caps)
debug(`${type} caps: ${caps.to_string()}`);
debug(`${type} caps: ${caps.to_string()}`);
}
if(type === 'video') {
const isShowVis = (
@@ -308,31 +307,21 @@ class ClapperWidget extends Gtk.Grid
updateTitle(mediaInfo)
{
let title = mediaInfo.get_title();
let title = this.player.customVideoTitle;
if(!title)
title = mediaInfo.get_title();
if(!title) {
const item = this.player.playlistWidget.getActiveRow();
title = item.filename;
}
this.refreshWindowTitle(title);
this.root.title = title;
this.revealerTop.title = title;
this.revealerTop.showTitle = true;
}
refreshWindowTitle(title)
{
const isFloating = !this.controlsRevealer.reveal_child;
const pipSuffix = ' - PiP';
const hasPipSuffix = title.endsWith(pipSuffix);
this.root.title = (isFloating && !hasPipSuffix)
? title + pipSuffix
: (!isFloating && hasPipSuffix)
? title.substring(0, title.length - pipSuffix.length)
: title;
}
updateTime()
{
if(

72
src/widgetRemote.js Normal file
View File

@@ -0,0 +1,72 @@
const { GObject, Gtk } = imports.gi;
const Buttons = imports.src.buttons;
const Misc = imports.src.misc;
const { PlayerRemote, ClapperState } = imports.src.playerRemote;
var WidgetRemote = GObject.registerClass({
GTypeName: 'ClapperWidgetRemote',
},
class ClapperWidgetRemote extends Gtk.Grid
{
_init(opts)
{
super._init({
halign: Gtk.Align.CENTER,
valign: Gtk.Align.CENTER,
});
Misc.loadCustomCss();
this.player = new PlayerRemote();
this.player.webclient.passMsgData = this.receiveWs.bind(this);
/* FIXME: create better way to add buttons for
* remote app without duplicating too much code */
this.togglePlayButton = new Buttons.IconToggleButton(
'media-playback-start-symbolic',
'media-playback-pause-symbolic'
);
this.togglePlayButton.remove_css_class('flat');
this.togglePlayButton.connect(
'clicked', () => this.sendWs('toggle_play')
);
this.attach(this.togglePlayButton, 0, 0, 1, 1);
}
sendWs(action, value)
{
const data = { action };
/* do not send "null" or "undefined"
* for faster network data transfer */
if(value != null)
data.value = value;
this.player.webclient.sendMessage(data);
}
receiveWs(action, value)
{
switch(action) {
case 'state_changed':
switch(value) {
case ClapperState.STOPPED:
case ClapperState.PAUSED:
this.togglePlayButton.setPrimaryIcon();
break;
case ClapperState.PLAYING:
this.togglePlayButton.setSecondaryIcon();
break;
default:
break;
}
break;
case 'close':
this.root.run_dispose();
break;
default:
break;
}
}
});

View File

@@ -3,11 +3,11 @@
<menu id="mainMenu">
<section>
<item>
<attribute name="label" translatable="yes">Open Files</attribute>
<attribute name="label" translatable="yes">Open Files...</attribute>
<attribute name="action">app.open_local</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Open URI</attribute>
<attribute name="label" translatable="yes">Open URI...</attribute>
<attribute name="action">app.open_uri</attribute>
</item>
</section>

View File

@@ -37,9 +37,9 @@
<property name="valign">center</property>
<property name="adjustment">speed_adjustment</property>
<marks>
<mark value="0.25" position="bottom">0.25×</mark>
<mark value="0.25" position="bottom">0.25x</mark>
<mark value="1" position="bottom" translatable="yes">Normal</mark>
<mark value="2" position="bottom">2×</mark>
<mark value="2" position="bottom">2x</mark>
</marks>
<style>
<class name="speedscale"/>

View File

@@ -5,6 +5,7 @@
<property name="resizable">True</property>
<property name="search-enabled">True</property>
<property name="destroy-with-parent">True</property>
<property name="can-swipe-back">True</property>
<property name="modal">True</property>
<child>
<object class="AdwPreferencesPage">
@@ -177,6 +178,45 @@
<property name="spin-adjustment">web_server_adjustment</property>
</object>
</child>
<child>
<object class="ClapperPrefsSwitch">
<property name="title" translatable="yes">Run web application in background</property>
<property name="subtitle" translatable="yes">Requires GTK compiled with Broadway backend</property>
<property name="schema-name">webapp-enabled</property>
</object>
</child>
<child>
<object class="ClapperPrefsSpin">
<property name="title" translatable="yes">Web application port</property>
<property name="schema-name">webapp-port</property>
<property name="spin-adjustment">web_app_adjustment</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="gtuber_group">
<property name="title" translatable="no">Gtuber</property>
<child>
<object class="ClapperPrefsSwitch">
<property name="title" translatable="yes">Prefer adaptive streaming</property>
<property name="schema-name">yt-adaptive-enabled</property>
</object>
</child>
<child>
<object class="ClapperPrefsCombo">
<property name="title" translatable="yes">Max quality</property>
<property name="schema-name">yt-quality-type</property>
<property name="model">
<object class="GtkStringList">
<items>
<item translatable="yes">Normal</item>
<item translatable="no">HFR</item>
</items>
</object>
</property>
</object>
</child>
</object>