52 Commits

Author SHA1 Message Date
Rafał Dzięgiel
2575b61cd0 Require GTK 4.6.0 for new plugin 2022-03-01 09:36:46 +01:00
Rafał Dzięgiel
9abf2fb11d wip4 2022-03-01 09:36:46 +01:00
Rafał Dzięgiel
167be4c37b wip3 2022-03-01 09:36:46 +01:00
Rafał Dzięgiel
74c4a079e6 wip2 2022-03-01 09:36:46 +01:00
Rafał Dzięgiel
8a597fffe7 WIP 2022-03-01 09:36:46 +01:00
Rafał Dzięgiel
159f96c984 Merge pull request #214 from Rafostar/libsoup3
libsoup3
2022-02-28 16:58:29 +01:00
Rafał Dzięgiel
96ad4fa3db RPM: Require libsoup3 2022-02-17 15:44:45 +01:00
Rafał Dzięgiel
b2e7bef8d4 flatpak: Update git actions runtime to 41
We need to update, so we can build libsoup3. Flathub ver will likely remain as 40 ver to avoid some problems and will be updated directly to 42 later.
2022-02-14 17:37:15 +01:00
Rafał Dzięgiel
40a1dc6960 flatpak: Build libsoup3
Now both Clapper and Gtuber lib require libsoup3, so build it until it will be available in runtime
2022-02-14 17:37:15 +01:00
Rafał Dzięgiel
c4bd604e17 Port to libsoup3
With all apps and modules/plugins porting itself to libsoup3 we also need to do so.

It would appear as a good idea to conditionally import "3.0" and fallback to "2.4"
bindings here, but its not as loaded GStreamer plugins might use libsoup3 already
and we cannot have both libsoup2 and libsoup3 in a single process.
2022-02-14 17:37:15 +01:00
Rafał Dzięgiel
83c0e3b598 Remove unfinished web application
It used Broadway as an easy "reuse the same code into web application" way for me, but Broadway
is not very good for this. This feature should be made using some better dedicated framework for
building websites (so it can work better and support different screen sizes).

All the WebSocket functionality needed for this will remain (and be documented at a later point
when expanded and stable), so if anyone would like to make such remote controlling app, will be
free to do so (outside of Clapper code).
2022-02-14 17:37:15 +01:00
Rafał Dzięgiel
54715023c0 flatpak: Update git actions manifest 2022-02-14 16:36:29 +01:00
Rafał Dzięgiel
95046ceba8 flatpak: Sync with Flathub 2022-02-14 16:06:36 +01:00
Rafał Dzięgiel
b189e24b5e Merge pull request #209 from simonsextras/swdeish-tranlation-nbfreygff
Full Swedish Translation
2022-01-21 12:14:22 +01:00
sastofficial
cb9e2ac979 Update sv.po 2022-01-21 09:35:17 +01:00
sastofficial
545d8826e0 Update sv.po 2022-01-19 13:46:08 +01:00
Rafał Dzięgiel
c2160198b9 Update LINGUAS file 2022-01-19 10:07:09 +01:00
Rafał Dzięgiel
aa7eae2417 New translations com.github.rafostar.Clapper.pot (French) (#208) 2022-01-19 10:00:56 +01:00
Rafał Dzięgiel
3192a32845 Merge pull request #206 from Rafostar/unicode
Use more unicode characters
2022-01-18 18:46:06 +01:00
Rafał Dzięgiel
706c783498 Use unicode U+2215 for division 2022-01-17 22:28:17 +01:00
Rafał Dzięgiel
8ba3ca4af6 Use unicode U+00D7 for multiplication 2022-01-17 21:01:23 +01:00
Rafał Dzięgiel
38efa7ab9f Use unicode U+2236 for time 2022-01-17 20:27:13 +01:00
Rafał Dzięgiel
d19ddbcaca Merge pull request #204 from Rafostar/devel
Misc gstclapper fixes
2022-01-17 11:00:28 +01:00
Rafał Dzięgiel
615f1553fb flatpak: Sync with Flathub 2022-01-17 10:19:29 +01:00
Rafał Dzięgiel
09c9e7560b New Crowdin updates (#201)
* New translations com.github.rafostar.Clapper.pot (Swedish)

* New translations com.github.rafostar.Clapper.pot (Russian)

* New translations com.github.rafostar.Clapper.pot (Chinese Simplified)

* New translations com.github.rafostar.Clapper.pot (Spanish)

* New translations com.github.rafostar.Clapper.pot (Spanish)

* New translations com.github.rafostar.Clapper.pot (Basque)
2022-01-17 08:56:59 +01:00
Rafał Dzięgiel
f40ce756ad Merge pull request #199 from majjejjam/master
change three dots to ellipsis
2022-01-16 14:10:49 +01:00
majjejjam
23d51be185 changed to ellipses in ui/clapper.ui 2022-01-15 18:20:13 +01:00
Rafał Dzięgiel
0db2a4a045 gstclapper: Fix plugin feature leak 2022-01-14 09:28:26 +01:00
Rafostar
469c06d22b gstclapper: Go into NULL state directly when stopped
Instead of waiting another 60 seconds for user, go into NULL state ASAP to free all resources and open devices.
2022-01-09 13:31:37 +01:00
Rafostar
3feaf225b5 gstclapper: Make tags writable before insert
Someone might be holding a copied media info in which case tag
list ref count is not 1. Use gst_tag_list_make_writable to
assure that ref count is 1, so we can insert more tags into the list.
2022-01-09 13:31:29 +01:00
majjejjam
fc8d881efd change three dots to ellipsis 2022-01-08 16:28:07 +01:00
Rafał Dzięgiel
ac76836fd7 Merge pull request #192 from Rafostar/yt-removal
YouTube code removal
2022-01-05 14:27:59 +01:00
Rafał Dzięgiel
f6a1aaf1dc Remove all YouTube code
It has been broken for quite some time. From now on this is gonna be left to handle for GStreamer plugins.
2021-12-22 08:12:55 +01:00
Rafał Dzięgiel
000dca82d9 flatpak: Sync with Flathub 2021-12-20 21:23:31 +01:00
Rafał Dzięgiel
7a508fef39 0.4.1 2021-12-20 11:33:19 +01:00
Rafał Dzięgiel
d465d9f150 Make floating window update its title earlier
Instead of waiting till animation finishes, update the window title right away when changing modes
2021-12-15 16:58:54 +01:00
Rafał Dzięgiel
5e4dfb322c Append "PiP" suffix to window title when in floating mode
We are gonna take advantage of this simple change elsewhere
2021-12-15 12:44:23 +01:00
Rafał Dzięgiel
0c561ab4b3 Also allow enabling gtuber code path via env
Having to use "gtuber" URI scheme might be inconvenient, so also allow to
whitelist it with "GST_PLUGIN_FEATURE_RANK=gtubersrc:300" env
2021-12-10 11:05:00 +01:00
Rafał Dzięgiel
46ce261524 widget: Make sure we have caps before logging them 2021-12-10 11:03:06 +01:00
Rafał Dzięgiel
50aac8cdd8 gstclapper: Merge global tags instead replacing them
There is no guarantee that received later tags also contain values from
earlier ones, as they might come from different element.
Combine them instead while replacing old values with newer ones.
2021-12-02 08:52:35 +01:00
Rafał Dzięgiel
810aea476f Use gtuber lib for URIs with "gtuber" scheme
Take a different code path when URI uses "gtuber" scheme.
This allows testing new WIP lib as an opt-in.
2021-11-29 22:33:53 +01:00
Rafał Dzięgiel
24905f1d60 flatpak: Build gtuber 2021-11-29 22:30:21 +01:00
Rafał Dzięgiel
82e3c9a52f prefs: Add can-swipe-back compat with latest libadwaita
Recent libadwaita has renamed "can-swipe-back" into "can-navigate-back".
Set both in JS code instead of UI file in order to support all libadwaita versions.

Fixes #185
2021-11-29 10:13:06 +01:00
Rafał Dzięgiel
654b8aaf60 prefs: Fix expander rows compat with latest libadwaita 2021-11-19 18:43:01 +01:00
Rafał Dzięgiel
3c0e33e4a4 css: Few override fixes for latest libadwaita 2021-11-19 18:40:34 +01:00
Rafał Dzięgiel
d2df1c3bd8 app: Use Adw.StyleManager to enable dark-theme
Latest libadwaita (for reasons unknown to me) totally ignores/breaks dark theme usage with gtk_application_prefer_dark_theme property. Lets just try using the new Adw.StyleManager without asking questions why.
2021-11-18 22:42:05 +01:00
Rafał Dzięgiel
af24073590 Update LINGUAS file 2021-11-18 15:37:48 +01:00
Rafał Dzięgiel
44cee14eb2 New translations com.github.rafostar.Clapper.pot (Portuguese) (#175) 2021-11-18 15:34:16 +01:00
Rafał Dzięgiel
b853685dd4 gstclapper: Fix decoder stream ID string leak 2021-10-22 13:11:42 +02:00
Rafał Dzięgiel
15461dd38a gstclapper: Fix video/audio decoder change detection
The video/audio decoder changed signal was not working correctly in case of
multiple streams with multiple decoders in single file.

We need to listen to the current-(video/audio) signal, when it changes find
corresponding "input-selector", get stream ID from its active pad and then
find the decoder in the pipeline that handles this stream ID. Similarly for
playbin3, but use stream ID from the "streams-selected" signal.
2021-10-22 11:05:08 +02:00
Rafał Dzięgiel
1c1989bc32 gstclapper: Fix GST_PLUGIN_FEATURE_RANK env usage
We change few default plugin ranks during init to whitelist them, but we do not update
their values from GST_PLUGIN_FEATURE_RANK env afterwards, so do that.
The ENV should be preferred over default config.
2021-10-21 11:21:07 +02:00
Rafał Dzięgiel
e9c9ae073f flatpak-nightly: Enable GPL gstreamer plugins 2021-10-19 10:23:41 +02:00
84 changed files with 3366 additions and 1516 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -103,16 +103,6 @@
<summary>Set PlayFlags for playbin</summary> <summary>Set PlayFlags for playbin</summary>
</key> </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 --> <!-- Other -->
<key name="window-size" type="s"> <key name="window-size" type="s">
<default>'[800, 490]'</default> <default>'[800, 490]'</default>

View File

@@ -48,6 +48,34 @@
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <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"> <release version="0.4.0" date="2021-09-12">
<description> <description>
<p>Changes:</p> <p>Changes:</p>

View File

@@ -152,7 +152,7 @@ struct _GstClapper
GstBus *bus; GstBus *bus;
GstState target_state, current_state; GstState target_state, current_state;
gboolean is_live; gboolean is_live;
GSource *tick_source, *ready_timeout_source; GSource *tick_source;
GstClockTime cached_duration; GstClockTime cached_duration;
gdouble rate; gdouble rate;
@@ -253,6 +253,9 @@ static void gst_clapper_audio_info_update (GstClapper * self,
static void gst_clapper_subtitle_info_update (GstClapper * self, static void gst_clapper_subtitle_info_update (GstClapper * self,
GstClapperStreamInfo * stream_info); GstClapperStreamInfo * stream_info);
static gboolean find_active_decoder_with_stream_id (GstClapper * self,
GstElementFactoryListType type, const gchar * stream_id);
/* For playbin3 */ /* For playbin3 */
static void gst_clapper_streams_info_create_from_collection (GstClapper * self, static void gst_clapper_streams_info_create_from_collection (GstClapper * self,
GstClapperMediaInfo * media_info, GstStreamCollection * collection); GstClapperMediaInfo * media_info, GstStreamCollection * collection);
@@ -1135,44 +1138,6 @@ remove_tick_source (GstClapper * self)
self->tick_source = NULL; 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 typedef struct
{ {
GstClapper *clapper; GstClapper *clapper;
@@ -1217,7 +1182,6 @@ emit_error (GstClapper * self, GError * err)
g_error_free (err); g_error_free (err);
remove_tick_source (self); remove_tick_source (self);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_NULL; self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_NULL; self->current_state = GST_STATE_NULL;
@@ -1862,6 +1826,17 @@ media_info_update (GstClapper * self, GstClapperMediaInfo * info)
"image_sample: %p", info->title, info->container, info->image_sample); "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 static void
tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{ {
@@ -1877,17 +1852,12 @@ tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) { if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) {
g_mutex_lock (&self->lock); g_mutex_lock (&self->lock);
if (self->media_info) { if (self->media_info) {
if (self->media_info->tags) merge_tags (&self->media_info->tags, 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); media_info_update (self, self->media_info);
g_mutex_unlock (&self->lock);
} else { } else {
if (self->global_tags) merge_tags (&self->global_tags, 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); gst_tag_list_unref (tags);
@@ -2050,6 +2020,7 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
{ {
GstClapper *self = GST_CLAPPER (user_data); GstClapper *self = GST_CLAPPER (user_data);
GstStreamCollection *collection = NULL; GstStreamCollection *collection = NULL;
gchar *video_sid, *audio_sid;
guint i, len; guint i, len;
gst_message_parse_streams_selected (msg, &collection); gst_message_parse_streams_selected (msg, &collection);
@@ -2098,7 +2069,22 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
*current_sid = g_strdup (stream_id); *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); 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 static gboolean
@@ -3009,11 +2995,12 @@ decoder_changed_signal_data_free (DecoderChangedSignalData * data)
static void static void
emit_decoder_changed (GstClapper * self, gchar * decoder_name, emit_decoder_changed (GstClapper * self, gchar * decoder_name,
gboolean is_video) GstElementFactoryListType type)
{ {
GstClapperSignalDispatcherFunc func = NULL; GstClapperSignalDispatcherFunc func = NULL;
if (is_video) { if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_VIDEO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 && signals[SIGNAL_VIDEO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_vdecoder, decoder_name) != 0) { g_strcmp0 (self->last_vdecoder, decoder_name) != 0) {
@@ -3021,7 +3008,8 @@ emit_decoder_changed (GstClapper * self, gchar * decoder_name,
g_free (self->last_vdecoder); g_free (self->last_vdecoder);
self->last_vdecoder = g_strdup (decoder_name); self->last_vdecoder = g_strdup (decoder_name);
} }
} else { } else if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_AUDIO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 && signals[SIGNAL_AUDIO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_adecoder, decoder_name) != 0) { g_strcmp0 (self->last_adecoder, decoder_name) != 0) {
@@ -3042,6 +3030,138 @@ 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 static void
element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self) element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
{ {
@@ -3054,13 +3174,6 @@ element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
if (plugin_name) { if (plugin_name) {
GST_INFO_OBJECT (self, "Plugin setup: %s", 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 */ /* TODO: Set plugin props */
} }
g_free (plugin_name); g_free (plugin_name);
@@ -3229,6 +3342,11 @@ gst_clapper_main (gpointer data)
G_CALLBACK (audio_tags_changed_cb), self); G_CALLBACK (audio_tags_changed_cb), self);
g_signal_connect (self->playbin, "text-tags-changed", g_signal_connect (self->playbin, "text-tags-changed",
G_CALLBACK (subtitle_tags_changed_cb), self); 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", g_signal_connect (self->playbin, "notify::volume",
@@ -3255,7 +3373,6 @@ gst_clapper_main (gpointer data)
gst_object_unref (bus); gst_object_unref (bus);
remove_tick_source (self); remove_tick_source (self);
remove_ready_timeout_source (self);
g_mutex_lock (&self->lock); g_mutex_lock (&self->lock);
if (self->media_info) { if (self->media_info) {
@@ -3324,6 +3441,104 @@ gst_clapper_has_plugin_with_features (const gchar * name)
return ret; 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 static void
gst_clapper_prepare_gstreamer (void) gst_clapper_prepare_gstreamer (void)
{ {
@@ -3346,6 +3561,9 @@ gst_clapper_prepare_gstreamer (void)
gst_clapper_set_feature_rank ("v4l2slvp8dec", GST_RANK_NONE); 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_clapper_gstreamer_prepared = TRUE;
GST_DEBUG ("GStreamer plugins prepared"); GST_DEBUG ("GStreamer plugins prepared");
} }
@@ -3422,7 +3640,6 @@ gst_clapper_play_internal (gpointer user_data)
} }
g_mutex_unlock (&self->lock); g_mutex_unlock (&self->lock);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_PLAYING; self->target_state = GST_STATE_PLAYING;
if (self->current_state < GST_STATE_PAUSED) if (self->current_state < GST_STATE_PAUSED)
@@ -3490,7 +3707,6 @@ gst_clapper_pause_internal (gpointer user_data)
tick_cb (self); tick_cb (self);
remove_tick_source (self); remove_tick_source (self);
remove_ready_timeout_source (self);
self->target_state = GST_STATE_PAUSED; self->target_state = GST_STATE_PAUSED;
@@ -3570,13 +3786,11 @@ gst_clapper_stop_internal (GstClapper * self, gboolean transient)
tick_cb (self); tick_cb (self);
remove_tick_source (self); remove_tick_source (self);
add_ready_timeout_source (self);
self->target_state = GST_STATE_NULL; self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_READY; self->current_state = GST_STATE_NULL;
self->is_live = FALSE; self->is_live = FALSE;
gst_bus_set_flushing (self->bus, TRUE); gst_bus_set_flushing (self->bus, TRUE);
gst_element_set_state (self->playbin, GST_STATE_READY); gst_element_set_state (self->playbin, GST_STATE_NULL);
gst_bus_set_flushing (self->bus, FALSE); gst_bus_set_flushing (self->bus, FALSE);
change_state (self, transient && self->app_state != GST_CLAPPER_STATE_STOPPED change_state (self, transient && self->app_state != GST_CLAPPER_STATE_STOPPED
? GST_CLAPPER_STATE_BUFFERING : GST_CLAPPER_STATE_STOPPED); ? GST_CLAPPER_STATE_BUFFERING : GST_CLAPPER_STATE_STOPPED);

View File

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

1
lib/gst/meson.build vendored
View File

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

681
lib/gst/plugin/gstclappersink.c vendored Normal file
View File

@@ -0,0 +1,681 @@
/*
* 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;
}

74
lib/gst/plugin/gstclappersink.h vendored Normal file
View File

@@ -0,0 +1,74 @@
/*
* 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

71
lib/gst/plugin/gstgtkutils.c vendored Normal file
View File

@@ -0,0 +1,71 @@
/*
* 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;
}

26
lib/gst/plugin/gstgtkutils.h vendored Normal file
View File

@@ -0,0 +1,26 @@
/*
* 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);

38
lib/gst/plugin/gstplugin.c vendored Normal file
View File

@@ -0,0 +1,38 @@
/*
* 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)

960
lib/gst/plugin/gtkclapperobject.c vendored Normal file
View File

@@ -0,0 +1,960 @@
/*
* 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;
}

139
lib/gst/plugin/gtkclapperobject.h vendored Normal file
View File

@@ -0,0 +1,139 @@
/*
* 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

63
lib/gst/plugin/meson.build vendored Normal file
View File

@@ -0,0 +1,63 @@
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' glib_req = '>= 2.56.0'
gst_req = '>= 1.18.0' gst_req = '>= 1.19.1'
api_version = '1.0' api_version = '1.0'
libversion = meson.project_version() libversion = meson.project_version()
@@ -200,6 +200,8 @@ gsttag_dep = dependency('gstreamer-tag-1.0', version: gst_req,
fallback: ['gst-plugins-base', 'tag_dep']) fallback: ['gst-plugins-base', 'tag_dep'])
gstvideo_dep = dependency('gstreamer-video-1.0', version: gst_req, gstvideo_dep = dependency('gstreamer-video-1.0', version: gst_req,
fallback: ['gst-plugins-base', 'video_dep']) fallback: ['gst-plugins-base', 'video_dep'])
gstallocators_dep = dependency('gstreamer-allocators-1.0', version: gst_req,
fallback : ['gst-plugins-base', 'allocators_dep'])
# GStreamer OpenGL # GStreamer OpenGL
gstgl_dep = dependency('gstreamer-gl-1.0', version: gst_req, gstgl_dep = dependency('gstreamer-gl-1.0', version: gst_req,
@@ -251,21 +253,49 @@ giounix_dep = dependency('gio-unix-2.0', version: glib_req, fallback: ['glib', '
cdata.set('DISABLE_ORC', 1) cdata.set('DISABLE_ORC', 1)
cdata.set('GST_ENABLE_EXTRA_CHECKS', get_option('devel-checks')) cdata.set('GST_ENABLE_EXTRA_CHECKS', get_option('devel-checks'))
cdata.set_quoted('GST_PACKAGE_RELEASE_DATETIME', 'Unknown')
configinc = include_directories('.') configinc = include_directories('.')
libsinc = include_directories('gst') libsinc = include_directories('gst')
gir = find_program('g-ir-scanner', required: true) gir = find_program('g-ir-scanner')
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**);' + \ 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_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_PATH_1_0", "", TRUE);' + \
'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \ 'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \
'gst_init(NULL,NULL);', '--quiet' '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') subdir('gst')
configure_file(output: 'config.h', configuration: cdata) configure_file(output: 'config.h', configuration: cdata)

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"app-id": "com.github.rafostar.Clapper", "app-id": "com.github.rafostar.Clapper",
"runtime": "org.gnome.Platform", "runtime": "org.gnome.Platform",
"runtime-version": "40", "runtime-version": "41",
"sdk": "org.gnome.Sdk", "sdk": "org.gnome.Sdk",
"command": "com.github.rafostar.Clapper", "command": "com.github.rafostar.Clapper",
"finish-args": [ "finish-args": [
@@ -33,13 +33,8 @@
"flathub/lib/libass.json", "flathub/lib/libass.json",
"flathub/lib/ffmpeg.json", "flathub/lib/ffmpeg.json",
"flathub/lib/uchardet.json", "flathub/lib/uchardet.json",
"testing/libsoup3.json",
"flathub/gstreamer-1.0/gstreamer.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/gtk4.json",
"flathub/lib/libadwaita.json", "flathub/lib/libadwaita.json",
"testing/gtuber.json", "testing/gtuber.json",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

459
po/eu.po Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,13 +96,6 @@ class ClapperAppBase extends Gtk.Application
if(accels) if(accels)
this.set_accels_for_action(`app.${name}`, 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; this.doneFirstActivate = true;
} }
}); });

View File

@@ -1,25 +0,0 @@
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() setInitialState()
{ {
this.label = '00:00/00:00'; this.label = '00000000';
} }
setFullscreenMode(isFullscreen, isMobileMonitor) setFullscreenMode(isFullscreen, isMobileMonitor)

6
src/controls.js vendored
View File

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

View File

@@ -1,70 +0,0 @@
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();
}
});

View File

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

View File

@@ -1,297 +0,0 @@
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));
});
});
}

View File

@@ -1,22 +0,0 @@
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,7 +1,6 @@
imports.gi.versions.Gdk = '4.0'; imports.gi.versions.Gdk = '4.0';
imports.gi.versions.Gtk = '4.0'; imports.gi.versions.Gtk = '4.0';
imports.gi.versions.Soup = '2.4'; imports.gi.versions.Soup = '3.0';
imports.gi.versions.Gtuber = '0.0';
pkg.initGettext(); pkg.initGettext();
pkg.initFormat(); pkg.initFormat();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
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');
}
});

View File

@@ -1,90 +0,0 @@
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('/'); this.remove_handler('/');
} }
_onWsConnection(server, connection) _onWsConnection(server, msg, path, connection)
{ {
debug('new WebSocket connection'); debug('new WebSocket connection');

View File

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

View File

@@ -1,72 +0,0 @@
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"> <menu id="mainMenu">
<section> <section>
<item> <item>
<attribute name="label" translatable="yes">Open Files...</attribute> <attribute name="label" translatable="yes">Open Files</attribute>
<attribute name="action">app.open_local</attribute> <attribute name="action">app.open_local</attribute>
</item> </item>
<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> <attribute name="action">app.open_uri</attribute>
</item> </item>
</section> </section>

View File

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

View File

@@ -5,7 +5,6 @@
<property name="resizable">True</property> <property name="resizable">True</property>
<property name="search-enabled">True</property> <property name="search-enabled">True</property>
<property name="destroy-with-parent">True</property> <property name="destroy-with-parent">True</property>
<property name="can-swipe-back">True</property>
<property name="modal">True</property> <property name="modal">True</property>
<child> <child>
<object class="AdwPreferencesPage"> <object class="AdwPreferencesPage">
@@ -178,45 +177,6 @@
<property name="spin-adjustment">web_server_adjustment</property> <property name="spin-adjustment">web_server_adjustment</property>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>