2 Commits

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

View File

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

View File

@@ -103,14 +103,14 @@
<summary>Set PlayFlags for playbin</summary>
</key>
<!-- YouTube -->
<!-- Gtuber -->
<key name="yt-adaptive-enabled" type="b">
<default>false</default>
<summary>Enable to use adaptive streaming for YouTube</summary>
<summary>Enable to use adaptive streaming</summary>
</key>
<key name="yt-quality-type" type="i">
<default>1</default>
<summary>Max YouTube video quality type</summary>
<summary>Max online video quality type</summary>
</key>
<!-- Other -->

View File

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

View File

@@ -253,9 +253,6 @@ static void gst_clapper_audio_info_update (GstClapper * self,
static void gst_clapper_subtitle_info_update (GstClapper * self,
GstClapperStreamInfo * stream_info);
static gboolean find_active_decoder_with_stream_id (GstClapper * self,
GstElementFactoryListType type, const gchar * stream_id);
/* For playbin3 */
static void gst_clapper_streams_info_create_from_collection (GstClapper * self,
GstClapperMediaInfo * media_info, GstStreamCollection * collection);
@@ -1865,15 +1862,6 @@ media_info_update (GstClapper * self, GstClapperMediaInfo * info)
"image_sample: %p", info->title, info->container, info->image_sample);
}
static void
merge_tags (GstTagList **my_tags, GstTagList *tags)
{
if (*my_tags)
gst_tag_list_insert (*my_tags, tags, GST_TAG_MERGE_REPLACE);
else
*my_tags = gst_tag_list_ref (tags);
}
static void
tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
@@ -1889,12 +1877,17 @@ tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) {
g_mutex_lock (&self->lock);
if (self->media_info) {
merge_tags (&self->media_info->tags, tags);
if (self->media_info->tags)
gst_tag_list_unref (self->media_info->tags);
self->media_info->tags = gst_tag_list_ref (tags);
media_info_update (self, self->media_info);
g_mutex_unlock (&self->lock);
} else {
merge_tags (&self->global_tags, tags);
if (self->global_tags)
gst_tag_list_unref (self->global_tags);
self->global_tags = gst_tag_list_ref (tags);
g_mutex_unlock (&self->lock);
}
g_mutex_unlock (&self->lock);
}
gst_tag_list_unref (tags);
@@ -2057,7 +2050,6 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
{
GstClapper *self = GST_CLAPPER (user_data);
GstStreamCollection *collection = NULL;
gchar *video_sid, *audio_sid;
guint i, len;
gst_message_parse_streams_selected (msg, &collection);
@@ -2106,22 +2098,7 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
*current_sid = g_strdup (stream_id);
}
video_sid = g_strdup (self->video_sid);
audio_sid = g_strdup (self->audio_sid);
g_mutex_unlock (&self->lock);
if (video_sid) {
find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO, video_sid);
g_free (video_sid);
}
if (audio_sid) {
find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, audio_sid);
g_free (audio_sid);
}
}
static gboolean
@@ -3032,12 +3009,11 @@ decoder_changed_signal_data_free (DecoderChangedSignalData * data)
static void
emit_decoder_changed (GstClapper * self, gchar * decoder_name,
GstElementFactoryListType type)
gboolean is_video)
{
GstClapperSignalDispatcherFunc func = NULL;
if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) {
if (is_video) {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_VIDEO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_vdecoder, decoder_name) != 0) {
@@ -3045,8 +3021,7 @@ emit_decoder_changed (GstClapper * self, gchar * decoder_name,
g_free (self->last_vdecoder);
self->last_vdecoder = g_strdup (decoder_name);
}
} else if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) ==
GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) {
} else {
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_AUDIO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 &&
g_strcmp0 (self->last_adecoder, decoder_name) != 0) {
@@ -3067,138 +3042,6 @@ emit_decoder_changed (GstClapper * self, gchar * decoder_name,
}
}
static gboolean
iterate_decoder_pads (GstClapper * self, GstElement * element,
const gchar * stream_id, GstElementFactoryListType type)
{
GstIterator *iter;
GValue value = { 0, };
gboolean found = FALSE;
iter = gst_element_iterate_src_pads (element);
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstPad *decoder_pad = g_value_get_object (&value);
gchar *decoder_stream_id = gst_pad_get_stream_id (decoder_pad);
GST_DEBUG_OBJECT (self, "Decoder stream: %s", decoder_stream_id);
/* In case of playbin3, pad may not be active yet */
if ((found = (g_strcmp0 (decoder_stream_id, stream_id) == 0
|| (!decoder_stream_id && self->use_playbin3)))) {
GstElementFactory *factory;
gchar *plugin_name;
factory = gst_element_get_factory (element);
plugin_name = gst_object_get_name (GST_OBJECT_CAST (factory));
if (plugin_name) {
GST_DEBUG_OBJECT (self, "Found decoder: %s", plugin_name);
emit_decoder_changed (self, plugin_name, type);
g_free (plugin_name);
}
}
g_free (decoder_stream_id);
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
return found;
}
static gboolean
find_active_decoder_with_stream_id (GstClapper * self, GstElementFactoryListType type,
const gchar * stream_id)
{
GstIterator *iter;
GValue value = { 0, };
gboolean found = FALSE;
GST_DEBUG_OBJECT (self, "Searching for decoder with stream: %s", stream_id);
iter = gst_bin_iterate_recurse (GST_BIN (self->playbin));
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstElement *element = g_value_get_object (&value);
GstElementFactory *factory = gst_element_get_factory (element);
if (factory && gst_element_factory_list_is_type (factory, type))
found = iterate_decoder_pads (self, element, stream_id, type);
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
return found;
}
static void
update_current_decoder (GstClapper *self, GstElementFactoryListType type)
{
GstIterator *iter;
GValue value = { 0, };
iter = gst_bin_iterate_all_by_element_factory_name (
GST_BIN (self->playbin), "input-selector");
while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) {
GstElement *element = g_value_get_object (&value);
GstPad *active_pad;
gboolean found = FALSE;
g_object_get (G_OBJECT (element), "active-pad", &active_pad, NULL);
if (active_pad) {
gchar *stream_id;
stream_id = gst_pad_get_stream_id (active_pad);
gst_object_unref (active_pad);
if (stream_id) {
found = find_active_decoder_with_stream_id (self, type, stream_id);
g_free (stream_id);
}
}
g_value_unset (&value);
if (found)
break;
}
gst_iterator_free (iter);
}
static void
current_video_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
GstClapper * self)
{
GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO;
update_current_decoder (self, type);
}
static void
current_audio_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
GstClapper * self)
{
GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER
| GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO;
update_current_decoder (self, type);
}
static void
element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
{
@@ -3211,6 +3054,13 @@ element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
if (plugin_name) {
GST_INFO_OBJECT (self, "Plugin setup: %s", plugin_name);
if (gst_element_factory_list_is_type (factory,
GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO))
emit_decoder_changed (self, plugin_name, TRUE);
else if (gst_element_factory_list_is_type (factory,
GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO))
emit_decoder_changed (self, plugin_name, FALSE);
/* TODO: Set plugin props */
}
g_free (plugin_name);
@@ -3379,11 +3229,6 @@ gst_clapper_main (gpointer data)
G_CALLBACK (audio_tags_changed_cb), self);
g_signal_connect (self->playbin, "text-tags-changed",
G_CALLBACK (subtitle_tags_changed_cb), self);
g_signal_connect (self->playbin, "notify::current-video",
G_CALLBACK (current_video_notify_cb), self);
g_signal_connect (self->playbin, "notify::current-audio",
G_CALLBACK (current_audio_notify_cb), self);
}
g_signal_connect (self->playbin, "notify::volume",
@@ -3479,103 +3324,6 @@ gst_clapper_has_plugin_with_features (const gchar * name)
return ret;
}
static gboolean
parse_feature_name (gchar * str, const gchar ** feature)
{
if (!str)
return FALSE;
g_strstrip (str);
if (str[0] != '\0') {
*feature = str;
return TRUE;
}
return FALSE;
}
static gboolean
parse_feature_rank (gchar * str, GstRank * rank)
{
if (!str)
return FALSE;
g_strstrip (str);
if (g_ascii_isdigit (str[0])) {
unsigned long l;
char *endptr;
l = strtoul (str, &endptr, 10);
if (endptr > str && endptr[0] == 0) {
*rank = (GstRank) l;
} else {
return FALSE;
}
} else if (g_ascii_strcasecmp (str, "NONE") == 0) {
*rank = GST_RANK_NONE;
} else if (g_ascii_strcasecmp (str, "MARGINAL") == 0) {
*rank = GST_RANK_MARGINAL;
} else if (g_ascii_strcasecmp (str, "SECONDARY") == 0) {
*rank = GST_RANK_SECONDARY;
} else if (g_ascii_strcasecmp (str, "PRIMARY") == 0) {
*rank = GST_RANK_PRIMARY;
} else if (g_ascii_strcasecmp (str, "MAX") == 0) {
*rank = (GstRank) G_MAXINT;
} else {
return FALSE;
}
return TRUE;
}
static void
_env_feature_rank_update (void)
{
const gchar *env;
gchar **split, **walk;
env = g_getenv ("GST_PLUGIN_FEATURE_RANK");
if (!env)
return;
split = g_strsplit (env, ",", 0);
for (walk = split; *walk; walk++) {
if (strchr (*walk, ':')) {
gchar **values;
values = g_strsplit (*walk, ":", 2);
if (values[0] && values[1]) {
GstRank rank;
const gchar *name;
if (parse_feature_name (values[0], &name)
&& parse_feature_rank (values[1], &rank)) {
GstPluginFeature *feature;
feature = gst_registry_find_feature (gst_registry_get (), name,
GST_TYPE_ELEMENT_FACTORY);
if (feature) {
GstRank old_rank;
old_rank = gst_plugin_feature_get_rank (feature);
if (old_rank != rank) {
gst_plugin_feature_set_rank (feature, rank);
GST_DEBUG ("Updated rank from env: %i -> %i for %s", old_rank, rank, name);
}
}
}
}
g_strfreev (values);
}
}
g_strfreev (split);
}
static void
gst_clapper_prepare_gstreamer (void)
{
@@ -3598,9 +3346,6 @@ gst_clapper_prepare_gstreamer (void)
gst_clapper_set_feature_rank ("v4l2slvp8dec", GST_RANK_NONE);
}
/* After setting defaults, update them from ENV */
_env_feature_rank_update ();
gst_clapper_gstreamer_prepared = TRUE;
GST_DEBUG ("GStreamer plugins prepared");
}

View File

@@ -1,5 +1,5 @@
project('com.github.rafostar.Clapper', 'c', 'cpp',
version: '0.4.1',
version: '0.4.0',
meson_version: '>= 0.50.0',
license: 'GPL-3.0-or-later',
default_options: [

View File

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

View File

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

View File

@@ -26,7 +26,7 @@
%global glib2_version 2.56.0
Name: clapper
Version: 0.4.1
Version: 0.4.0
Release: 1%{?dist}
Summary: Simple and modern GNOME media player
@@ -129,9 +129,6 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_libdir}/%{appname}/
%changelog
* Mon Dec 20 2021 Rafostar <rafostar.github@gmail.com> - 0.4.1-1
- New version
* Sun Sep 12 2021 Rafostar <rafostar.github@gmail.com> - 0.4.0-1
- New version

View File

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

View File

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

View File

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

View File

@@ -1,151 +0,0 @@
/* Copyright (C) 2012-present by fent
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
const jsEmptyStr = `(?:''|"")`;
const reverseStr = ':function\\(a\\)\\{' +
'(?:return )?a\\.reverse\\(\\)' +
'\\}';
const sliceStr = ':function\\(a,b\\)\\{' +
'return a\\.slice\\(b\\)' +
'\\}';
const spliceStr = ':function\\(a,b\\)\\{' +
'a\\.splice\\(0,b\\)' +
'\\}';
const swapStr = ':function\\(a,b\\)\\{' +
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
'\\}';
const actionsObjRegexp = new RegExp(
`var (${jsVarStr})=\\{((?:(?:${
jsKeyStr}${reverseStr}|${
jsKeyStr}${sliceStr}|${
jsKeyStr}${spliceStr}|${
jsKeyStr}${swapStr
}),?\\r?\\n?)+)\\};`);
const actionsFuncRegexp = new RegExp(`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
`((?:(?:a=)?${jsVarStr}`}${
jsPropStr
}\\(a,\\d+\\);)+)` +
`return a\\.join\\(${jsEmptyStr}\\)` +
`\\}`);
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
const swapHeadAndPosition = (arr, position) => {
const first = arr[0];
arr[0] = arr[position % arr.length];
arr[position] = first;
return arr;
}
function decipher(sig, tokens) {
sig = sig.split('');
tokens = tokens.split(',');
for(let i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i], pos;
switch (token[0]) {
case 'r':
sig = sig.reverse();
break;
case 'w':
pos = ~~token.slice(1);
sig = swapHeadAndPosition(sig, pos);
break;
case 's':
pos = ~~token.slice(1);
sig = sig.slice(pos);
break;
case 'p':
pos = ~~token.slice(1);
sig.splice(0, pos);
break;
}
}
return sig.join('');
};
function extractActions(body) {
const objResult = actionsObjRegexp.exec(body);
const funcResult = actionsFuncRegexp.exec(body);
if(!objResult || !funcResult)
return null;
const obj = objResult[1].replace(/\$/g, '\\$');
const objBody = objResult[2].replace(/\$/g, '\\$');
const funcBody = funcResult[1].replace(/\$/g, '\\$');
let result = reverseRegexp.exec(objBody);
const reverseKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = sliceRegexp.exec(objBody);
const sliceKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = spliceRegexp.exec(objBody);
const spliceKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = swapRegexp.exec(objBody);
const swapKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
const myreg = `(?:a=)?${obj
}(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
`\\(a,(\\d+)\\)`;
const tokenizeRegexp = new RegExp(myreg, 'g');
const tokens = [];
while((result = tokenizeRegexp.exec(funcBody)) !== null) {
const key = result[1] || result[2] || result[3];
const pos = result[4];
switch (key) {
case swapKey:
tokens.push(`w${result[4]}`);
break;
case reverseKey:
tokens.push('r');
break;
case sliceKey:
tokens.push(`s${result[4]}`);
break;
case spliceKey:
tokens.push(`p${result[4]}`);
break;
}
}
return tokens.join(',');
}

View File

@@ -14,22 +14,13 @@ const clapperDebugger = new Debug.Debugger('Clapper', {
}),
high_precision: true,
});
clapperDebugger.enabled = (
var enabled = (
clapperDebugger.enabled
|| G_DEBUG_ENV != null
&& G_DEBUG_ENV.includes('Clapper')
);
const ytDebugger = new Debug.Debugger('YouTube', {
name_printer: new Ink.Printer({
font: Ink.Font.BOLD,
color: Ink.Color.RED
}),
time_printer: new Ink.Printer({
color: Ink.Color.LIGHT_BLUE
}),
high_precision: true,
});
clapperDebugger.enabled = enabled;
function _logStructured(debuggerName, msg, level)
{
@@ -43,23 +34,12 @@ function _logStructured(debuggerName, msg, level)
function _debug(debuggerName, msg)
{
if(msg.message) {
_logStructured(
debuggerName,
msg.message,
_logStructured(debuggerName, msg.message,
GLib.LogLevelFlags.LEVEL_CRITICAL
);
return;
}
switch(debuggerName) {
case 'Clapper':
clapperDebugger.debug(msg);
break;
case 'YouTube':
ytDebugger.debug(msg);
break;
}
clapperDebugger.debug(msg);
}
function debug(msg)
@@ -67,12 +47,12 @@ function debug(msg)
_debug('Clapper', msg);
}
function ytDebug(msg)
{
_debug('YouTube', msg);
}
function warn(msg)
{
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_WARNING);
}
function message(msg)
{
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_MESSAGE);
}

297
src/gtuber.js Normal file
View File

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

View File

@@ -1,6 +1,7 @@
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();
pkg.initFormat();

View File

@@ -1,6 +1,7 @@
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();

View File

@@ -1,7 +1,8 @@
const { Gio, GLib, Gdk, Gtk } = imports.gi;
const Debug = imports.src.debug;
const { debug } = Debug;
const { debug, message } = Debug;
const failedImports = [];
var appName = 'Clapper';
var appId = 'com.github.rafostar.Clapper';
@@ -28,6 +29,23 @@ const subsKeys = Object.keys(subsTitles);
let inhibitCookie;
function tryImport(libName)
{
let lib = null;
try {
lib = imports.gi[libName];
}
catch(err) {
if(!failedImports.includes(libName)) {
failedImports.push(libName);
message(err.message);
}
}
return lib;
}
function getResourceUri(path)
{
const res = `file://${pkg.pkgdatadir}/${path}`;
@@ -224,22 +242,3 @@ function getIsTouch(gesture)
return false;
}
}
function encodeHTML(text)
{
return text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function decodeURIPlus(uri)
{
return decodeURI(uri.replace(/\+/g, ' '));
}
function isHex(num)
{
return Boolean(num.match(/[0-9a-f]+$/i));
}

View File

@@ -1,8 +1,8 @@
const { Adw, Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const YouTube = imports.src.youtube;
const Gtuber = imports.src.gtuber;
const { PlaylistWidget } = imports.src.playlist;
const { WebApp } = imports.src.webApp;
@@ -45,7 +45,6 @@ class ClapperPlayer extends GstClapper.Clapper
this.webserver = null;
this.webapp = null;
this.ytClient = null;
this.playlistWidget = new PlaylistWidget();
this.seekDone = true;
@@ -74,7 +73,6 @@ class ClapperPlayer extends GstClapper.Clapper
set_and_bind_settings()
{
const settingsToSet = [
'dark-theme',
'after-playback',
'seeking-mode',
'audio-offset',
@@ -143,24 +141,13 @@ class ClapperPlayer extends GstClapper.Clapper
set_uri(uri)
{
this.customVideoTitle = null;
Gtuber.cancelFetching();
if(Misc.getUriProtocol(uri) !== 'file') {
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
if(!isYouTubeUri)
return super.set_uri(uri);
if(!this.ytClient)
this.ytClient = new YouTube.YouTubeClient();
const { root } = this.widget;
const surface = root.get_surface();
const monitor = root.display.get_monitor_at_surface(surface);
this.ytClient.getPlaybackDataAsync(videoId, monitor)
.then(data => {
this.customVideoTitle = data.title;
super.set_uri(data.uri);
if(Gtuber.mightHandleUri(uri)) {
Gtuber.parseUriPromise(uri, this)
.then(res => {
this.customVideoTitle = res.title;
super.set_uri(res.uri);
})
.catch(debug);
@@ -658,19 +645,6 @@ class ClapperPlayer extends GstClapper.Clapper
break;
}
break;
case 'dark-theme':
/* TODO: Remove libadwaita alpha2 compat someday */
if (Adw.StyleManager != null) {
const styleManager = Adw.StyleManager.get_default();
styleManager.color_scheme = (settings.get_boolean(key))
? Adw.ColorScheme.FORCE_DARK
: Adw.ColorScheme.FORCE_LIGHT;
}
else {
const gtkSettings = Gtk.Settings.get_default();
gtkSettings.gtk_application_prefer_dark_theme = settings.get_boolean(key);
}
break;
case 'render-shadows':
root = this.widget.get_root();
if(!root) break;

View File

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

View File

@@ -321,8 +321,6 @@ class ClapperControlsRevealer extends Gtk.Revealer
const isStick = (isFloating && settings.get_boolean('floating-stick'));
DBus.shellWindowEval('stick', isStick);
this.root.child.refreshWindowTitle(this.root.title);
}
_onControlsRevealed()

View File

@@ -4,7 +4,6 @@ const Debug = imports.src.debug;
const Dialogs = imports.src.dialogs;
const Misc = imports.src.misc;
const { Player } = imports.src.player;
const YouTube = imports.src.youtube;
const Revealers = imports.src.revealers;
const { debug } = Debug;
@@ -278,8 +277,7 @@ class ClapperWidget extends Gtk.Grid
if(currStream && type !== 'subtitle') {
const caps = currStream.get_caps();
if (caps)
debug(`${type} caps: ${caps.to_string()}`);
debug(`${type} caps: ${caps.to_string()}`);
}
if(type === 'video') {
const isShowVis = (
@@ -319,24 +317,11 @@ class ClapperWidget extends Gtk.Grid
title = item.filename;
}
this.refreshWindowTitle(title);
this.root.title = title;
this.revealerTop.title = title;
this.revealerTop.showTitle = true;
}
refreshWindowTitle(title)
{
const isFloating = !this.controlsRevealer.reveal_child;
const pipSuffix = ' - PiP';
const hasPipSuffix = title.endsWith(pipSuffix);
this.root.title = (isFloating && !hasPipSuffix)
? title + pipSuffix
: (!isFloating && hasPipSuffix)
? title.substring(0, title.length - pipSuffix.length)
: title;
}
updateTime()
{
if(
@@ -817,12 +802,10 @@ class ClapperWidget extends Gtk.Grid
{
const dropTarget = new Gtk.DropTarget({
actions: Gdk.DragAction.COPY | Gdk.DragAction.MOVE,
preload: true,
});
dropTarget.set_gtypes([GObject.TYPE_STRING]);
dropTarget.connect('motion', this._onDataMotion.bind(this));
dropTarget.connect('drop', this._onDataDrop.bind(this));
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
return dropTarget;
}
@@ -1023,36 +1006,6 @@ class ClapperWidget extends Gtk.Grid
this.posY = posY;
}
_onDropValueNotify(dropTarget)
{
if(!dropTarget.value)
return;
const uris = dropTarget.value.split(/\r?\n/);
const firstUri = uris[0];
if(uris.length > 1 || !Gst.uri_is_valid(firstUri))
return;
/* Check if user is dragging a YouTube link */
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(firstUri);
if(!isYouTubeUri) return;
/* Since this is a YouTube video,
* create YT client if it was not created yet */
if(!this.player.ytClient)
this.player.ytClient = new YouTube.YouTubeClient();
const { ytClient } = this.player;
/* Speed up things by prefetching new video info before drop */
if(
!ytClient.compareLastVideoId(videoId)
&& ytClient.downloadingVideoId !== videoId
)
ytClient.getVideoInfoPromise(videoId).catch(debug);
}
_onDataMotion(dropTarget, x, y)
{
return Gdk.DragAction.MOVE;

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +0,0 @@
var QualityType = {
0: 'normal',
1: 'hfr',
};
const Itags = {
video: {
h264: {
normal: {
240: 133,
360: 134,
480: 135,
720: 136,
1080: 137,
},
hfr: {
720: 298,
1080: 299,
},
},
},
audio: {
aac: [140],
opus: [249, 250, 251],
},
combined: {
360: 18,
720: 22,
},
hls: {
240: 92,
360: 93,
480: 94,
720: 95,
1080: 96,
}
};
function _appendItagArray(arr, opts, formats)
{
const keys = Object.keys(formats);
for(let fmt of keys) {
arr.push(formats[fmt]);
if(
fmt >= opts.height
|| Math.floor(fmt * 16 / 9) >= opts.width
)
break;
}
return arr;
}
function getDashItags(opts)
{
const allowed = {
video: [],
audio: (opts.codec === 'h264')
? Itags.audio.aac
: Itags.audio.opus
};
const types = Object.keys(Itags.video[opts.codec]);
for(let type of types) {
const formats = Itags.video[opts.codec][type];
_appendItagArray(allowed.video, opts, formats);
if(type === opts.type)
break;
}
return allowed;
}
function getCombinedItags(opts)
{
return _appendItagArray([], opts, Itags.combined);
}
function getHLSItags(opts)
{
return _appendItagArray([], opts, Itags.hls);
}

View File

@@ -5,6 +5,7 @@
<property name="resizable">True</property>
<property name="search-enabled">True</property>
<property name="destroy-with-parent">True</property>
<property name="can-swipe-back">True</property>
<property name="modal">True</property>
<child>
<object class="AdwPreferencesPage">
@@ -196,8 +197,8 @@
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="no">YouTube</property>
<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>