mirror of
https://github.com/Rafostar/clapper.git
synced 2025-09-04 18:31:59 +02:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0ab0b66825 | ||
|
d901eb4712 | ||
|
fe03719b38 | ||
|
f0ea7ae798 | ||
|
380236b8ba | ||
|
e721130a63 | ||
|
eaf090d2e2 | ||
|
87115f43d7 | ||
|
33a5ec18fa | ||
|
ab8cafa0b8 | ||
|
62b6de6db2 | ||
|
643c2029d0 | ||
|
9799783ee5 | ||
|
457cbde25e | ||
|
2fd94fdc70 | ||
|
3a998fb91e | ||
|
b02f54a3a6 | ||
|
ca7b44092e | ||
|
adbcfecb5e | ||
|
a717e481e8 |
@@ -62,8 +62,7 @@ It can be enabled from inside player preferences dialog inside `Advanced -> GStr
|
|||||||
Since the whole app is rendered using your GPU, users of VERY weak GPUs might want to disable the "render window shadows" option to have more GPU power available for non-fullscreen video rendering.
|
Since the whole app is rendered using your GPU, users of VERY weak GPUs might want to disable the "render window shadows" option to have more GPU power available for non-fullscreen video rendering.
|
||||||
|
|
||||||
## Other Questions?
|
## Other Questions?
|
||||||
Feel free to ask me any questions.<br>
|
Feel free to ask me any questions. Come and talk on Matrix: [#clapper-player:matrix.org](https://matrix.to/#/#clapper-player:matrix.org)
|
||||||
Use either GitHub [discussions](https://github.com/Rafostar/clapper/discussions) or come and talk on Matrix: **#clapper-player:matrix.org**
|
|
||||||
|
|
||||||
## Special Thanks
|
## Special Thanks
|
||||||
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.
|
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.
|
||||||
|
@@ -102,7 +102,11 @@
|
|||||||
<!-- YouTube -->
|
<!-- YouTube -->
|
||||||
<key name="yt-adaptive-enabled" type="b">
|
<key name="yt-adaptive-enabled" type="b">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
<summary>Enable to use adaptive streaming</summary>
|
<summary>Enable to use adaptive streaming for YouTube</summary>
|
||||||
|
</key>
|
||||||
|
<key name="yt-quality-type" type="s">
|
||||||
|
<default>"hfr"</default>
|
||||||
|
<summary>Max YouTube video quality type</summary>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
<!-- Other -->
|
<!-- Other -->
|
||||||
|
@@ -52,6 +52,24 @@
|
|||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.2.1" date="2021-04-19">
|
||||||
|
<description>
|
||||||
|
<p>Player:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fix missing top left menu buttons on some system configurations</li>
|
||||||
|
<li>Fix potential video sink deadlock</li>
|
||||||
|
<li>Do not show mobile controls transition on launch</li>
|
||||||
|
<li>Show tooltip with full playlist item text on hover</li>
|
||||||
|
</ul>
|
||||||
|
<p>YouTube:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Auto select best matching resolution for used monitor</li>
|
||||||
|
<li>Added some YouTube related preferences</li>
|
||||||
|
<li>Added support for live HLS videos</li>
|
||||||
|
<li>Added support for non-adaptive live HLS streaming</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.2.0" date="2021-04-13">
|
<release version="0.2.0" date="2021-04-13">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
|
16
lib/gst/clapper/gstclapper.c
vendored
16
lib/gst/clapper/gstclapper.c
vendored
@@ -880,9 +880,9 @@ static void
|
|||||||
emit_media_info_updated (GstClapper * self)
|
emit_media_info_updated (GstClapper * self)
|
||||||
{
|
{
|
||||||
MediaInfoUpdatedSignalData *data = g_new (MediaInfoUpdatedSignalData, 1);
|
MediaInfoUpdatedSignalData *data = g_new (MediaInfoUpdatedSignalData, 1);
|
||||||
|
self->needs_info_update = FALSE;
|
||||||
data->clapper = g_object_ref (self);
|
data->clapper = g_object_ref (self);
|
||||||
g_mutex_lock (&self->lock);
|
g_mutex_lock (&self->lock);
|
||||||
self->needs_info_update = FALSE;
|
|
||||||
data->info = gst_clapper_media_info_copy (self->media_info);
|
data->info = gst_clapper_media_info_copy (self->media_info);
|
||||||
g_mutex_unlock (&self->lock);
|
g_mutex_unlock (&self->lock);
|
||||||
|
|
||||||
@@ -946,9 +946,12 @@ change_state (GstClapper * self, GstClapperState state)
|
|||||||
gst_clapper_state_get_name (state));
|
gst_clapper_state_get_name (state));
|
||||||
self->app_state = state;
|
self->app_state = state;
|
||||||
|
|
||||||
if (state == GST_CLAPPER_STATE_STOPPED && self->rate != 1.0) {
|
if (state == GST_CLAPPER_STATE_STOPPED) {
|
||||||
self->rate = 1.0;
|
self->needs_info_update = FALSE;
|
||||||
emit_rate_notify (self);
|
if (self->rate != 1.0) {
|
||||||
|
self->rate = 1.0;
|
||||||
|
emit_rate_notify (self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
|
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
|
||||||
@@ -2343,9 +2346,10 @@ stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
|
|||||||
g_mutex_lock (&self->lock);
|
g_mutex_lock (&self->lock);
|
||||||
info =
|
info =
|
||||||
gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id);
|
gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id);
|
||||||
if (info)
|
if (info) {
|
||||||
gst_clapper_stream_info_update_from_stream (self, info, stream);
|
gst_clapper_stream_info_update_from_stream (self, info, stream);
|
||||||
emit_update = (self->needs_info_update && GST_IS_CLAPPER_VIDEO_INFO (info));
|
emit_update = (self->needs_info_update && GST_IS_CLAPPER_VIDEO_INFO (info));
|
||||||
|
}
|
||||||
g_mutex_unlock (&self->lock);
|
g_mutex_unlock (&self->lock);
|
||||||
|
|
||||||
if (emit_update)
|
if (emit_update)
|
||||||
|
@@ -151,13 +151,9 @@ gtk_clapper_gl_widget_size_allocate (GtkWidget * widget,
|
|||||||
GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget);
|
GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget);
|
||||||
gint scale_factor = gtk_widget_get_scale_factor (widget);
|
gint scale_factor = gtk_widget_get_scale_factor (widget);
|
||||||
|
|
||||||
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget);
|
|
||||||
|
|
||||||
clapper_widget->scaled_width = width * scale_factor;
|
clapper_widget->scaled_width = width * scale_factor;
|
||||||
clapper_widget->scaled_height = height * scale_factor;
|
clapper_widget->scaled_height = height * scale_factor;
|
||||||
|
|
||||||
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget);
|
|
||||||
|
|
||||||
gtk_gl_area_queue_render (GTK_GL_AREA (widget));
|
gtk_gl_area_queue_render (GTK_GL_AREA (widget));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +562,7 @@ gtk_clapper_gl_widget_render (GtkGLArea * widget, GdkGLContext * context)
|
|||||||
GtkClapperGLWidgetPrivate *priv = clapper_widget->priv;
|
GtkClapperGLWidgetPrivate *priv = clapper_widget->priv;
|
||||||
const GstGLFuncs *gl;
|
const GstGLFuncs *gl;
|
||||||
|
|
||||||
GTK_CLAPPER_GL_WIDGET_LOCK (widget);
|
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget);
|
||||||
|
|
||||||
/* Draw black with GDK context when priv is not available yet.
|
/* Draw black with GDK context when priv is not available yet.
|
||||||
GTK calls render with GDK context already active. */
|
GTK calls render with GDK context already active. */
|
||||||
@@ -672,7 +668,7 @@ done:
|
|||||||
if (priv->other_context)
|
if (priv->other_context)
|
||||||
gst_gl_context_activate (priv->other_context, FALSE);
|
gst_gl_context_activate (priv->other_context, FALSE);
|
||||||
|
|
||||||
GTK_CLAPPER_GL_WIDGET_UNLOCK (widget);
|
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
project('com.github.rafostar.Clapper', 'c', 'cpp',
|
project('com.github.rafostar.Clapper', 'c', 'cpp',
|
||||||
version: '0.2.0',
|
version: '0.2.1',
|
||||||
meson_version: '>= 0.50.0',
|
meson_version: '>= 0.50.0',
|
||||||
license: 'GPL3',
|
license: 'GPL3',
|
||||||
default_options: [
|
default_options: [
|
||||||
|
@@ -2,7 +2,7 @@ Format: 3.0 (quilt)
|
|||||||
Source: clapper
|
Source: clapper
|
||||||
Binary: clapper
|
Binary: clapper
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Version: 0.2.0
|
Version: 0.2.1
|
||||||
Maintainer: Rafostar <rafostar.github@gmail.com>
|
Maintainer: Rafostar <rafostar.github@gmail.com>
|
||||||
Build-Depends: debhelper (>= 10),
|
Build-Depends: debhelper (>= 10),
|
||||||
meson (>= 0.50),
|
meson (>= 0.50),
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
clapper (0.2.0) unstable; urgency=low
|
clapper (0.2.1) unstable; urgency=low
|
||||||
|
|
||||||
* New version
|
* New version
|
||||||
|
|
||||||
-- Rafostar <rafostar.github@gmail.com> Tue, 13 Apr 2021 09:39:00 +0100
|
-- Rafostar <rafostar.github@gmail.com> Mon, 19 Apr 2021 09:39:00 +0100
|
||||||
|
@@ -26,7 +26,7 @@
|
|||||||
%global glib2_version 2.56.0
|
%global glib2_version 2.56.0
|
||||||
|
|
||||||
Name: clapper
|
Name: clapper
|
||||||
Version: 0.2.0
|
Version: 0.2.1
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Simple and modern GNOME media player
|
Summary: Simple and modern GNOME media player
|
||||||
|
|
||||||
@@ -126,6 +126,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
|
|||||||
%{_libdir}/%{appname}/
|
%{_libdir}/%{appname}/
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Apr 19 2021 Rafostar <rafostar.github@gmail.com> - 0.2.1-1
|
||||||
|
- New version
|
||||||
|
|
||||||
* Tue Apr 13 2021 Rafostar <rafostar.github@gmail.com> - 0.2.0-1
|
* Tue Apr 13 2021 Rafostar <rafostar.github@gmail.com> - 0.2.0-1
|
||||||
- New version
|
- New version
|
||||||
|
|
||||||
|
4
src/controls.js
vendored
4
src/controls.js
vendored
@@ -18,6 +18,8 @@ class ClapperControls extends Gtk.Box
|
|||||||
can_focus: false,
|
can_focus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.minFullViewWidth = 560;
|
||||||
|
|
||||||
this.currentPosition = 0;
|
this.currentPosition = 0;
|
||||||
this.currentDuration = 0;
|
this.currentDuration = 0;
|
||||||
this.isPositionDragging = false;
|
this.isPositionDragging = false;
|
||||||
@@ -473,7 +475,7 @@ class ClapperControls extends Gtk.Box
|
|||||||
|
|
||||||
_onPlayerResize(width, height)
|
_onPlayerResize(width, height)
|
||||||
{
|
{
|
||||||
const isMobile = (width < 560);
|
const isMobile = (width < this.minFullViewWidth);
|
||||||
if(this.isMobile === isMobile)
|
if(this.isMobile === isMobile)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@@ -228,6 +228,10 @@ class ClapperPrefsDialog extends Gtk.Dialog
|
|||||||
{
|
{
|
||||||
title: 'Network',
|
title: 'Network',
|
||||||
widget: Prefs.NetworkPage,
|
widget: Prefs.NetworkPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'YouTube',
|
||||||
|
widget: Prefs.YouTubePage,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@@ -151,7 +151,7 @@ class ClapperHeaderBarBase extends Gtk.Box
|
|||||||
|
|
||||||
for(let name of layoutArr) {
|
for(let name of layoutArr) {
|
||||||
/* Menu might be named "appmenu" */
|
/* Menu might be named "appmenu" */
|
||||||
if(!menuAdded && (!name || name === 'appmenu'))
|
if(!menuAdded && (!name || name === 'appmenu' || name === 'icon'))
|
||||||
name = 'menu';
|
name = 'menu';
|
||||||
|
|
||||||
const widget = this[`${name}Widget`];
|
const widget = this[`${name}Widget`];
|
||||||
|
269
src/player.js
269
src/player.js
@@ -1,24 +1,50 @@
|
|||||||
const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
const { Gdk, Gio, GLib, 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 YouTube = imports.src.youtube;
|
const YouTube = imports.src.youtube;
|
||||||
const { PlayerBase } = imports.src.playerBase;
|
const { PlaylistWidget } = imports.src.playlist;
|
||||||
|
const { WebApp } = imports.src.webApp;
|
||||||
|
|
||||||
const { debug } = Debug;
|
const { debug } = Debug;
|
||||||
const { settings } = Misc;
|
const { settings } = Misc;
|
||||||
|
|
||||||
|
let WebServer;
|
||||||
|
|
||||||
var Player = GObject.registerClass(
|
var Player = GObject.registerClass(
|
||||||
class ClapperPlayer extends PlayerBase
|
class ClapperPlayer extends GstClapper.Clapper
|
||||||
{
|
{
|
||||||
_init()
|
_init()
|
||||||
{
|
{
|
||||||
super._init();
|
const gtk4plugin = new GstClapper.ClapperGtk4Plugin();
|
||||||
|
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
|
||||||
|
glsinkbin.sink = gtk4plugin.video_sink;
|
||||||
|
|
||||||
|
const dispatcher = new GstClapper.ClapperGMainContextSignalDispatcher();
|
||||||
|
const renderer = new GstClapper.ClapperVideoOverlayVideoRenderer({
|
||||||
|
video_sink: glsinkbin
|
||||||
|
});
|
||||||
|
|
||||||
|
super._init({
|
||||||
|
signal_dispatcher: dispatcher,
|
||||||
|
video_renderer: renderer
|
||||||
|
});
|
||||||
|
|
||||||
|
this.widget = gtk4plugin.video_sink.widget;
|
||||||
|
this.widget.add_css_class('videowidget');
|
||||||
|
|
||||||
|
this.state = GstClapper.ClapperState.STOPPED;
|
||||||
|
this.visualization_enabled = false;
|
||||||
|
|
||||||
|
this.webserver = null;
|
||||||
|
this.webapp = null;
|
||||||
|
this.playlistWidget = new PlaylistWidget();
|
||||||
|
|
||||||
this.seek_done = true;
|
this.seek_done = true;
|
||||||
this.needsFastSeekRestore = false;
|
this.needsFastSeekRestore = false;
|
||||||
this.customVideoTitle = null;
|
this.customVideoTitle = null;
|
||||||
|
|
||||||
|
this.windowMapped = false;
|
||||||
this.canAutoFullscreen = false;
|
this.canAutoFullscreen = false;
|
||||||
this.playOnFullscreen = false;
|
this.playOnFullscreen = false;
|
||||||
this.quitOnStop = false;
|
this.quitOnStop = false;
|
||||||
@@ -32,15 +58,93 @@ class ClapperPlayer extends PlayerBase
|
|||||||
keyController.connect('key-released', this._onWidgetKeyReleased.bind(this));
|
keyController.connect('key-released', this._onWidgetKeyReleased.bind(this));
|
||||||
this.widget.add_controller(keyController);
|
this.widget.add_controller(keyController);
|
||||||
|
|
||||||
|
this.set_all_plugins_ranks();
|
||||||
|
this.set_initial_config();
|
||||||
|
this.set_and_bind_settings();
|
||||||
|
|
||||||
this.connect('state-changed', this._onStateChanged.bind(this));
|
this.connect('state-changed', this._onStateChanged.bind(this));
|
||||||
this.connect('uri-loaded', this._onUriLoaded.bind(this));
|
this.connect('uri-loaded', this._onUriLoaded.bind(this));
|
||||||
this.connect('end-of-stream', this._onStreamEnded.bind(this));
|
this.connect('end-of-stream', this._onStreamEnded.bind(this));
|
||||||
this.connect('warning', this._onPlayerWarning.bind(this));
|
this.connect('warning', this._onPlayerWarning.bind(this));
|
||||||
this.connect('error', this._onPlayerError.bind(this));
|
this.connect('error', this._onPlayerError.bind(this));
|
||||||
|
|
||||||
|
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
|
||||||
|
|
||||||
this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this));
|
this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_and_bind_settings()
|
||||||
|
{
|
||||||
|
const settingsToSet = [
|
||||||
|
'seeking-mode',
|
||||||
|
'audio-offset',
|
||||||
|
'subtitle-offset',
|
||||||
|
'play-flags',
|
||||||
|
'webserver-enabled'
|
||||||
|
];
|
||||||
|
|
||||||
|
for(let key of settingsToSet)
|
||||||
|
this._onSettingsKeyChanged(settings, key);
|
||||||
|
|
||||||
|
const flag = Gio.SettingsBindFlags.GET;
|
||||||
|
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_initial_config()
|
||||||
|
{
|
||||||
|
this.set_mute(false);
|
||||||
|
|
||||||
|
/* FIXME: change into option in preferences */
|
||||||
|
const pipeline = this.get_pipeline();
|
||||||
|
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_all_plugins_ranks()
|
||||||
|
{
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
/* Set empty plugin list if someone messed it externally */
|
||||||
|
try {
|
||||||
|
data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||||
|
if(!Array.isArray(data))
|
||||||
|
throw new Error('plugin ranking data is not an array!');
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
debug(err);
|
||||||
|
settings.set_string('plugin-ranking', "[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let plugin of data) {
|
||||||
|
if(!plugin.apply || !plugin.name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.set_plugin_rank(plugin.name, plugin.rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_plugin_rank(name, rank)
|
||||||
|
{
|
||||||
|
const gstRegistry = Gst.Registry.get();
|
||||||
|
const feature = gstRegistry.lookup_feature(name);
|
||||||
|
if(!feature)
|
||||||
|
return debug(`plugin unavailable: ${name}`);
|
||||||
|
|
||||||
|
const oldRank = feature.get_rank();
|
||||||
|
if(rank === oldRank)
|
||||||
|
return;
|
||||||
|
|
||||||
|
feature.set_rank(rank);
|
||||||
|
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_black(isEnabled)
|
||||||
|
{
|
||||||
|
this.widget.ignore_textures = isEnabled;
|
||||||
|
|
||||||
|
if(this.state !== GstClapper.ClapperState.PLAYING)
|
||||||
|
this.widget.queue_render();
|
||||||
|
}
|
||||||
|
|
||||||
set_uri(uri)
|
set_uri(uri)
|
||||||
{
|
{
|
||||||
this.customVideoTitle = null;
|
this.customVideoTitle = null;
|
||||||
@@ -54,7 +158,11 @@ class ClapperPlayer extends PlayerBase
|
|||||||
if(!this.ytClient)
|
if(!this.ytClient)
|
||||||
this.ytClient = new YouTube.YouTubeClient();
|
this.ytClient = new YouTube.YouTubeClient();
|
||||||
|
|
||||||
this.ytClient.getPlaybackDataAsync(videoId)
|
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 => {
|
.then(data => {
|
||||||
this.customVideoTitle = data.title;
|
this.customVideoTitle = data.title;
|
||||||
super.set_uri(data.uri);
|
super.set_uri(data.uri);
|
||||||
@@ -121,10 +229,9 @@ class ClapperPlayer extends PlayerBase
|
|||||||
this.playlistWidget.addItem(uri);
|
this.playlistWidget.addItem(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
/* If not mapped yet, first track will play after map */
|
||||||
if(!firstTrack) return;
|
if(this.windowMapped)
|
||||||
|
this._playFirstTrack();
|
||||||
firstTrack.activate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_subtitles(source)
|
set_subtitles(source)
|
||||||
@@ -179,7 +286,7 @@ class ClapperPlayer extends PlayerBase
|
|||||||
|
|
||||||
seek_seconds(seconds)
|
seek_seconds(seconds)
|
||||||
{
|
{
|
||||||
this.seek(seconds * 1000000000);
|
this.seek(seconds * Gst.SECOND);
|
||||||
}
|
}
|
||||||
|
|
||||||
seek_chapter(seconds)
|
seek_chapter(seconds)
|
||||||
@@ -189,12 +296,9 @@ class ClapperPlayer extends PlayerBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME: Remove this check when GstPlay(er) have set_seek_mode function */
|
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
||||||
if(this.set_seek_mode) {
|
this.seekingMode = 'normal';
|
||||||
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
this.needsFastSeekRestore = true;
|
||||||
this.seekingMode = 'normal';
|
|
||||||
this.needsFastSeekRestore = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.seek_seconds(seconds);
|
this.seek_seconds(seconds);
|
||||||
}
|
}
|
||||||
@@ -251,6 +355,14 @@ class ClapperPlayer extends PlayerBase
|
|||||||
this[action]();
|
this[action]();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitWs(action, value)
|
||||||
|
{
|
||||||
|
if(!this.webserver)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.webserver.sendMessage({ action, value });
|
||||||
|
}
|
||||||
|
|
||||||
receiveWs(action, value)
|
receiveWs(action, value)
|
||||||
{
|
{
|
||||||
switch(action) {
|
switch(action) {
|
||||||
@@ -275,7 +387,7 @@ class ClapperPlayer extends PlayerBase
|
|||||||
clapperWidget.toggleFullscreen();
|
clapperWidget.toggleFullscreen();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
super.receiveWs(action, value);
|
debug(`unhandled WebSocket action: ${action}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -291,6 +403,14 @@ class ClapperPlayer extends PlayerBase
|
|||||||
: Gst.filename_to_uri(source);
|
: Gst.filename_to_uri(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_playFirstTrack()
|
||||||
|
{
|
||||||
|
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
||||||
|
if(!firstTrack) return;
|
||||||
|
|
||||||
|
firstTrack.activate();
|
||||||
|
}
|
||||||
|
|
||||||
_performCloseCleanup(window)
|
_performCloseCleanup(window)
|
||||||
{
|
{
|
||||||
window.disconnect(this.closeRequestSignal);
|
window.disconnect(this.closeRequestSignal);
|
||||||
@@ -312,8 +432,8 @@ class ClapperPlayer extends PlayerBase
|
|||||||
|
|
||||||
let resumeInfo = {};
|
let resumeInfo = {};
|
||||||
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
|
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
|
||||||
const resumeTime = Math.floor(this.position / 1000000000);
|
const resumeTime = Math.floor(this.position / Gst.SECOND);
|
||||||
const resumeDuration = this.duration / 1000000000;
|
const resumeDuration = this.duration / Gst.SECOND;
|
||||||
|
|
||||||
/* Do not save resume info when video is very short,
|
/* Do not save resume info when video is very short,
|
||||||
* just started or almost finished */
|
* just started or almost finished */
|
||||||
@@ -522,6 +642,12 @@ class ClapperPlayer extends PlayerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onWindowMap(window)
|
||||||
|
{
|
||||||
|
this.windowMapped = true;
|
||||||
|
this._playFirstTrack();
|
||||||
|
}
|
||||||
|
|
||||||
_onCloseRequest(window)
|
_onCloseRequest(window)
|
||||||
{
|
{
|
||||||
this._performCloseCleanup(window);
|
this._performCloseCleanup(window);
|
||||||
@@ -532,4 +658,109 @@ class ClapperPlayer extends PlayerBase
|
|||||||
this.quitOnStop = true;
|
this.quitOnStop = true;
|
||||||
this.stop();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSettingsKeyChanged(settings, key)
|
||||||
|
{
|
||||||
|
let root, value, action;
|
||||||
|
|
||||||
|
switch(key) {
|
||||||
|
case 'seeking-mode':
|
||||||
|
this.seekingMode = settings.get_string('seeking-mode');
|
||||||
|
switch(this.seekingMode) {
|
||||||
|
case 'fast':
|
||||||
|
this.set_seek_mode(GstClapper.ClapperSeekMode.FAST);
|
||||||
|
break;
|
||||||
|
case 'accurate':
|
||||||
|
this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'render-shadows':
|
||||||
|
root = this.widget.get_root();
|
||||||
|
if(!root) break;
|
||||||
|
|
||||||
|
const gpuClass = 'gpufriendly';
|
||||||
|
const renderShadows = settings.get_boolean(key);
|
||||||
|
const hasShadows = !root.has_css_class(gpuClass);
|
||||||
|
|
||||||
|
if(renderShadows === hasShadows)
|
||||||
|
break;
|
||||||
|
|
||||||
|
action = (renderShadows) ? 'remove' : 'add';
|
||||||
|
root[action + '_css_class'](gpuClass);
|
||||||
|
break;
|
||||||
|
case 'audio-offset':
|
||||||
|
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
|
||||||
|
this.set_audio_video_offset(value);
|
||||||
|
debug(`set audio-video offset: ${value}`);
|
||||||
|
break;
|
||||||
|
case 'subtitle-offset':
|
||||||
|
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
|
||||||
|
this.set_subtitle_video_offset(value);
|
||||||
|
debug(`set subtitle-video offset: ${value}`);
|
||||||
|
break;
|
||||||
|
case 'dark-theme':
|
||||||
|
root = this.widget.get_root();
|
||||||
|
if(!root) break;
|
||||||
|
|
||||||
|
root.application._onThemeChanged(Gtk.Settings.get_default());
|
||||||
|
break;
|
||||||
|
case 'play-flags':
|
||||||
|
const initialFlags = this.pipeline.flags;
|
||||||
|
const settingsFlags = settings.get_int(key);
|
||||||
|
|
||||||
|
if(initialFlags === settingsFlags)
|
||||||
|
break;
|
||||||
|
|
||||||
|
this.pipeline.flags = settingsFlags;
|
||||||
|
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
|
||||||
|
break;
|
||||||
|
case 'webserver-enabled':
|
||||||
|
case 'webapp-enabled':
|
||||||
|
const webserverEnabled = settings.get_boolean('webserver-enabled');
|
||||||
|
|
||||||
|
if(webserverEnabled) {
|
||||||
|
if(!WebServer) {
|
||||||
|
/* Probably most users will not use this,
|
||||||
|
* so conditional import for faster startup */
|
||||||
|
WebServer = imports.src.webServer.WebServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.webserver) {
|
||||||
|
this.webserver = new WebServer(settings.get_int('webserver-port'));
|
||||||
|
this.webserver.passMsgData = this.receiveWs.bind(this);
|
||||||
|
}
|
||||||
|
this.webserver.startListening();
|
||||||
|
|
||||||
|
const webappEnabled = settings.get_boolean('webapp-enabled');
|
||||||
|
|
||||||
|
if(!this.webapp && !webappEnabled)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if(webappEnabled) {
|
||||||
|
if(!this.webapp)
|
||||||
|
this.webapp = new WebApp();
|
||||||
|
|
||||||
|
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(this.webserver) {
|
||||||
|
/* remote app will close when connection is lost
|
||||||
|
* which will cause the daemon to close too */
|
||||||
|
this.webserver.stopListening();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'webserver-port':
|
||||||
|
if(!this.webserver)
|
||||||
|
break;
|
||||||
|
|
||||||
|
this.webserver.setListeningPort(settings.get_int(key));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,237 +0,0 @@
|
|||||||
const { Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
|
||||||
const Debug = imports.src.debug;
|
|
||||||
const Misc = imports.src.misc;
|
|
||||||
const { PlaylistWidget } = imports.src.playlist;
|
|
||||||
const { WebApp } = imports.src.webApp;
|
|
||||||
|
|
||||||
const { debug } = Debug;
|
|
||||||
const { settings } = Misc;
|
|
||||||
|
|
||||||
let WebServer;
|
|
||||||
|
|
||||||
var PlayerBase = GObject.registerClass(
|
|
||||||
class ClapperPlayerBase extends GstClapper.Clapper
|
|
||||||
{
|
|
||||||
_init()
|
|
||||||
{
|
|
||||||
const gtk4plugin = new GstClapper.ClapperGtk4Plugin();
|
|
||||||
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
|
|
||||||
glsinkbin.sink = gtk4plugin.video_sink;
|
|
||||||
|
|
||||||
const dispatcher = new GstClapper.ClapperGMainContextSignalDispatcher();
|
|
||||||
const renderer = new GstClapper.ClapperVideoOverlayVideoRenderer({
|
|
||||||
video_sink: glsinkbin
|
|
||||||
});
|
|
||||||
|
|
||||||
super._init({
|
|
||||||
signal_dispatcher: dispatcher,
|
|
||||||
video_renderer: renderer
|
|
||||||
});
|
|
||||||
|
|
||||||
this.widget = gtk4plugin.video_sink.widget;
|
|
||||||
this.widget.add_css_class('videowidget');
|
|
||||||
|
|
||||||
this.state = GstClapper.ClapperState.STOPPED;
|
|
||||||
this.visualization_enabled = false;
|
|
||||||
|
|
||||||
this.webserver = null;
|
|
||||||
this.webapp = null;
|
|
||||||
this.playlistWidget = new PlaylistWidget();
|
|
||||||
|
|
||||||
this.set_all_plugins_ranks();
|
|
||||||
this.set_initial_config();
|
|
||||||
this.set_and_bind_settings();
|
|
||||||
|
|
||||||
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
set_and_bind_settings()
|
|
||||||
{
|
|
||||||
const settingsToSet = [
|
|
||||||
'seeking-mode',
|
|
||||||
'audio-offset',
|
|
||||||
'subtitle-offset',
|
|
||||||
'play-flags',
|
|
||||||
'webserver-enabled'
|
|
||||||
];
|
|
||||||
|
|
||||||
for(let key of settingsToSet)
|
|
||||||
this._onSettingsKeyChanged(settings, key);
|
|
||||||
|
|
||||||
const flag = Gio.SettingsBindFlags.GET;
|
|
||||||
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
set_initial_config()
|
|
||||||
{
|
|
||||||
this.set_mute(false);
|
|
||||||
|
|
||||||
/* FIXME: change into option in preferences */
|
|
||||||
const pipeline = this.get_pipeline();
|
|
||||||
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_all_plugins_ranks()
|
|
||||||
{
|
|
||||||
let data = [];
|
|
||||||
|
|
||||||
/* Set empty plugin list if someone messed it externally */
|
|
||||||
try {
|
|
||||||
data = JSON.parse(settings.get_string('plugin-ranking'));
|
|
||||||
if(!Array.isArray(data))
|
|
||||||
throw new Error('plugin ranking data is not an array!');
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
debug(err);
|
|
||||||
settings.set_string('plugin-ranking', "[]");
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let plugin of data) {
|
|
||||||
if(!plugin.apply || !plugin.name)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
this.set_plugin_rank(plugin.name, plugin.rank);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set_plugin_rank(name, rank)
|
|
||||||
{
|
|
||||||
const gstRegistry = Gst.Registry.get();
|
|
||||||
const feature = gstRegistry.lookup_feature(name);
|
|
||||||
if(!feature)
|
|
||||||
return debug(`plugin unavailable: ${name}`);
|
|
||||||
|
|
||||||
const oldRank = feature.get_rank();
|
|
||||||
if(rank === oldRank)
|
|
||||||
return;
|
|
||||||
|
|
||||||
feature.set_rank(rank);
|
|
||||||
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_black(isEnabled)
|
|
||||||
{
|
|
||||||
this.widget.ignore_textures = isEnabled;
|
|
||||||
|
|
||||||
if(this.state !== GstClapper.ClapperState.PLAYING)
|
|
||||||
this.widget.queue_render();
|
|
||||||
}
|
|
||||||
|
|
||||||
emitWs(action, value)
|
|
||||||
{
|
|
||||||
if(!this.webserver)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.webserver.sendMessage({ action, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
receiveWs(action, value)
|
|
||||||
{
|
|
||||||
debug(`unhandled WebSocket action: ${action}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSettingsKeyChanged(settings, key)
|
|
||||||
{
|
|
||||||
let root, value, action;
|
|
||||||
|
|
||||||
switch(key) {
|
|
||||||
case 'seeking-mode':
|
|
||||||
this.seekingMode = settings.get_string('seeking-mode');
|
|
||||||
switch(this.seekingMode) {
|
|
||||||
case 'fast':
|
|
||||||
this.set_seek_mode(GstClapper.ClapperSeekMode.FAST);
|
|
||||||
break;
|
|
||||||
case 'accurate':
|
|
||||||
this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'render-shadows':
|
|
||||||
root = this.widget.get_root();
|
|
||||||
if(!root) break;
|
|
||||||
|
|
||||||
const gpuClass = 'gpufriendly';
|
|
||||||
const renderShadows = settings.get_boolean(key);
|
|
||||||
const hasShadows = !root.has_css_class(gpuClass);
|
|
||||||
|
|
||||||
if(renderShadows === hasShadows)
|
|
||||||
break;
|
|
||||||
|
|
||||||
action = (renderShadows) ? 'remove' : 'add';
|
|
||||||
root[action + '_css_class'](gpuClass);
|
|
||||||
break;
|
|
||||||
case 'audio-offset':
|
|
||||||
value = Math.round(settings.get_double(key) * -1000000);
|
|
||||||
this.set_audio_video_offset(value);
|
|
||||||
debug(`set audio-video offset: ${value}`);
|
|
||||||
break;
|
|
||||||
case 'subtitle-offset':
|
|
||||||
value = Math.round(settings.get_double(key) * -1000000);
|
|
||||||
this.set_subtitle_video_offset(value);
|
|
||||||
debug(`set subtitle-video offset: ${value}`);
|
|
||||||
break;
|
|
||||||
case 'dark-theme':
|
|
||||||
root = this.widget.get_root();
|
|
||||||
if(!root) break;
|
|
||||||
|
|
||||||
root.application._onThemeChanged(Gtk.Settings.get_default());
|
|
||||||
break;
|
|
||||||
case 'play-flags':
|
|
||||||
const initialFlags = this.pipeline.flags;
|
|
||||||
const settingsFlags = settings.get_int(key);
|
|
||||||
|
|
||||||
if(initialFlags === settingsFlags)
|
|
||||||
break;
|
|
||||||
|
|
||||||
this.pipeline.flags = settingsFlags;
|
|
||||||
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
|
|
||||||
break;
|
|
||||||
case 'webserver-enabled':
|
|
||||||
case 'webapp-enabled':
|
|
||||||
const webserverEnabled = settings.get_boolean('webserver-enabled');
|
|
||||||
|
|
||||||
if(webserverEnabled) {
|
|
||||||
if(!WebServer) {
|
|
||||||
/* Probably most users will not use this,
|
|
||||||
* so conditional import for faster startup */
|
|
||||||
WebServer = imports.src.webServer.WebServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.webserver) {
|
|
||||||
this.webserver = new WebServer(settings.get_int('webserver-port'));
|
|
||||||
this.webserver.passMsgData = this.receiveWs.bind(this);
|
|
||||||
}
|
|
||||||
this.webserver.startListening();
|
|
||||||
|
|
||||||
const webappEnabled = settings.get_boolean('webapp-enabled');
|
|
||||||
|
|
||||||
if(!this.webapp && !webappEnabled)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if(webappEnabled) {
|
|
||||||
if(!this.webapp)
|
|
||||||
this.webapp = new WebApp();
|
|
||||||
|
|
||||||
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(this.webserver) {
|
|
||||||
/* remote app will close when connection is lost
|
|
||||||
* which will cause the daemon to close too */
|
|
||||||
this.webserver.stopListening();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'webserver-port':
|
|
||||||
if(!this.webserver)
|
|
||||||
break;
|
|
||||||
|
|
||||||
this.webserver.setListeningPort(settings.get_int(key));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@@ -119,6 +119,7 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
|
|||||||
this.isLocalFile = true;
|
this.isLocalFile = true;
|
||||||
}
|
}
|
||||||
this.filename = filename || uri;
|
this.filename = filename || uri;
|
||||||
|
this.set_tooltip_text(this.filename);
|
||||||
|
|
||||||
const box = new Gtk.Box({
|
const box = new Gtk.Box({
|
||||||
orientation: Gtk.Orientation.HORIZONTAL,
|
orientation: Gtk.Orientation.HORIZONTAL,
|
||||||
|
16
src/prefs.js
16
src/prefs.js
@@ -132,6 +132,22 @@ class ClapperNetworkPage extends PrefsBase.Grid
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var YouTubePage = GObject.registerClass(
|
||||||
|
class ClapperYouTubePage extends PrefsBase.Grid
|
||||||
|
{
|
||||||
|
_init()
|
||||||
|
{
|
||||||
|
super._init();
|
||||||
|
|
||||||
|
this.addTitle('YouTube');
|
||||||
|
this.addCheckButton('Prefer adaptive streaming', 'yt-adaptive-enabled');
|
||||||
|
this.addComboBoxText('Max quality', [
|
||||||
|
['normal', "Normal"],
|
||||||
|
['hfr', "HFR"],
|
||||||
|
], 'yt-quality-type');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var GStreamerPage = GObject.registerClass(
|
var GStreamerPage = GObject.registerClass(
|
||||||
class ClapperGStreamerPage extends PrefsBase.Grid
|
class ClapperGStreamerPage extends PrefsBase.Grid
|
||||||
{
|
{
|
||||||
|
@@ -370,6 +370,18 @@ class ClapperButtonsRevealer extends Gtk.Revealer
|
|||||||
this.get_child().append(widget);
|
this.get_child().append(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
revealInstantly(isReveal)
|
||||||
|
{
|
||||||
|
if(this.child_revealed === isReveal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const initialDuration = this.transition_duration;
|
||||||
|
|
||||||
|
this.transition_duration = 0;
|
||||||
|
this.reveal_child = isReveal;
|
||||||
|
this.transition_duration = initialDuration;
|
||||||
|
}
|
||||||
|
|
||||||
_setRotateClass(icon, isAdd)
|
_setRotateClass(icon, isAdd)
|
||||||
{
|
{
|
||||||
const cssClass = 'halfrotate';
|
const cssClass = 'halfrotate';
|
||||||
@@ -388,7 +400,8 @@ class ClapperButtonsRevealer extends Gtk.Revealer
|
|||||||
|
|
||||||
_onRevealChild(button)
|
_onRevealChild(button)
|
||||||
{
|
{
|
||||||
this._setRotateClass(button.child, true);
|
if(this.reveal_child !== this.child_revealed)
|
||||||
|
this._setRotateClass(button.child, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChildRevealed(button)
|
_onChildRevealed(button)
|
||||||
|
@@ -371,7 +371,7 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pos = Math.floor(start / 1000000) / 1000;
|
const pos = Math.floor(start / Gst.MSECOND) / 1000;
|
||||||
const tags = subentry.get_tags();
|
const tags = subentry.get_tags();
|
||||||
|
|
||||||
this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null);
|
this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null);
|
||||||
@@ -467,7 +467,7 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
|
|
||||||
_onPlayerDurationChanged(player, duration)
|
_onPlayerDurationChanged(player, duration)
|
||||||
{
|
{
|
||||||
const durationSeconds = duration / 1000000000;
|
const durationSeconds = duration / Gst.SECOND;
|
||||||
const durationFloor = Math.floor(durationSeconds);
|
const durationFloor = Math.floor(durationSeconds);
|
||||||
|
|
||||||
/* Sometimes GstPlayer might re-emit
|
/* Sometimes GstPlayer might re-emit
|
||||||
@@ -515,7 +515,7 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const positionSeconds = Math.round(position / 1000000000);
|
const positionSeconds = Math.round(position / Gst.SECOND);
|
||||||
if(positionSeconds === this.controls.currentPosition)
|
if(positionSeconds === this.controls.currentPosition)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -541,6 +541,12 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
if(width === this.layoutWidth)
|
if(width === this.layoutWidth)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
/* Launch without showing revealers transitions on mobile width */
|
||||||
|
if(!this.layoutWidth && width < this.controls.minFullViewWidth) {
|
||||||
|
for(let revealer of this.controls.revealersArr)
|
||||||
|
revealer.revealInstantly(false);
|
||||||
|
}
|
||||||
|
|
||||||
this.layoutWidth = width;
|
this.layoutWidth = width;
|
||||||
|
|
||||||
if(this.isFullscreenMode)
|
if(this.isFullscreenMode)
|
||||||
@@ -572,6 +578,8 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
|
|
||||||
surface.connect('notify::state', this._onStateNotify.bind(this));
|
surface.connect('notify::state', this._onStateNotify.bind(this));
|
||||||
surface.connect('layout', this._onLayoutUpdate.bind(this));
|
surface.connect('layout', this._onLayoutUpdate.bind(this));
|
||||||
|
|
||||||
|
this.player._onWindowMap(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearTimeout(name)
|
_clearTimeout(name)
|
||||||
|
189
src/youtube.js
189
src/youtube.js
@@ -3,6 +3,7 @@ const Dash = imports.src.dash;
|
|||||||
const Debug = imports.src.debug;
|
const Debug = imports.src.debug;
|
||||||
const FileOps = imports.src.fileOps;
|
const FileOps = imports.src.fileOps;
|
||||||
const Misc = imports.src.misc;
|
const Misc = imports.src.misc;
|
||||||
|
const YTItags = imports.src.youtubeItags;
|
||||||
const YTDL = imports.src.assets['node-ytdl-core'];
|
const YTDL = imports.src.assets['node-ytdl-core'];
|
||||||
|
|
||||||
const debug = Debug.ytDebug;
|
const debug = Debug.ytDebug;
|
||||||
@@ -304,7 +305,7 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlaybackDataAsync(videoId)
|
async getPlaybackDataAsync(videoId, monitor)
|
||||||
{
|
{
|
||||||
const info = await this.getVideoInfoPromise(videoId).catch(debug);
|
const info = await this.getVideoInfoPromise(videoId).catch(debug);
|
||||||
|
|
||||||
@@ -312,28 +313,40 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
throw new Error('no YouTube video info');
|
throw new Error('no YouTube video info');
|
||||||
|
|
||||||
let uri = null;
|
let uri = null;
|
||||||
const dashInfo = await this.getDashInfoAsync(info).catch(debug);
|
const itagOpts = {
|
||||||
|
width: monitor.geometry.width * monitor.scale_factor,
|
||||||
|
height: monitor.geometry.height * monitor.scale_factor,
|
||||||
|
codec: 'h264',
|
||||||
|
type: settings.get_string('yt-quality-type'),
|
||||||
|
adaptive: settings.get_boolean('yt-adaptive-enabled'),
|
||||||
|
};
|
||||||
|
|
||||||
if(dashInfo) {
|
uri = await this.getHLSUriAsync(info, itagOpts);
|
||||||
debug('parsed video info to dash info');
|
|
||||||
const dash = Dash.generateDash(dashInfo);
|
|
||||||
|
|
||||||
if(dash) {
|
if(!uri) {
|
||||||
debug('got dash data');
|
const dashInfo = await this.getDashInfoAsync(info, itagOpts).catch(debug);
|
||||||
|
|
||||||
const dashFile = await FileOps.saveFilePromise(
|
if(dashInfo) {
|
||||||
'tmp', null, 'clapper.mpd', dash
|
debug('parsed video info to dash info');
|
||||||
).catch(debug);
|
const dash = Dash.generateDash(dashInfo);
|
||||||
|
|
||||||
if(dashFile)
|
if(dash) {
|
||||||
uri = dashFile.get_uri();
|
debug('got dash data');
|
||||||
|
|
||||||
debug('got dash file');
|
const dashFile = await FileOps.saveFilePromise(
|
||||||
|
'tmp', null, 'clapper.mpd', dash
|
||||||
|
).catch(debug);
|
||||||
|
|
||||||
|
if(dashFile)
|
||||||
|
uri = dashFile.get_uri();
|
||||||
|
|
||||||
|
debug('got dash file');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!uri)
|
if(!uri)
|
||||||
uri = this.getBestCombinedUri(info);
|
uri = this.getBestCombinedUri(info, itagOpts);
|
||||||
|
|
||||||
if(!uri)
|
if(!uri)
|
||||||
throw new Error('no YouTube video URI');
|
throw new Error('no YouTube video URI');
|
||||||
@@ -349,7 +362,61 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
return { uri, title };
|
return { uri, title };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDashInfoAsync(info)
|
async getHLSUriAsync(info, itagOpts)
|
||||||
|
{
|
||||||
|
const isLive = (
|
||||||
|
info.videoDetails.isLiveContent
|
||||||
|
&& (!info.videoDetails.lengthSeconds
|
||||||
|
|| Number(info.videoDetails.lengthSeconds) <= 0)
|
||||||
|
);
|
||||||
|
debug(`video is live: ${isLive}`);
|
||||||
|
|
||||||
|
/* YouTube only uses HLS for live content */
|
||||||
|
if(!isLive)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const hlsUri = info.streamingData.hlsManifestUrl;
|
||||||
|
if(!hlsUri) {
|
||||||
|
/* HLS may be unavailable on finished live streams */
|
||||||
|
debug('no HLS manifest URL');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!itagOpts.adaptive) {
|
||||||
|
const result = await this._downloadDataPromise(hlsUri).catch(debug);
|
||||||
|
if(!result || !result.data) {
|
||||||
|
debug(new Error('HLS manifest download failed'));
|
||||||
|
return hlsUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hlsArr = result.data.split('\n');
|
||||||
|
const hlsStreams = [];
|
||||||
|
|
||||||
|
let index = hlsArr.length;
|
||||||
|
while(index--) {
|
||||||
|
const url = hlsArr[index];
|
||||||
|
if(!Gst.Uri.is_valid(url))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const itagIndex = url.indexOf('/itag/') + 6;
|
||||||
|
const itag = url.substring(itagIndex, itagIndex + 2);
|
||||||
|
|
||||||
|
hlsStreams.push({ itag, url });
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`obtaining HLS itags for resolution: ${itagOpts.width}x${itagOpts.height}`);
|
||||||
|
const hlsItags = YTItags.getHLSItags(itagOpts);
|
||||||
|
debug(`HLS itags: ${JSON.stringify(hlsItags)}`);
|
||||||
|
|
||||||
|
const hlsStream = this.getBestStreamFromItags(hlsStreams, hlsItags);
|
||||||
|
if(hlsStream)
|
||||||
|
return hlsStream.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hlsUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDashInfoAsync(info, itagOpts)
|
||||||
{
|
{
|
||||||
if(
|
if(
|
||||||
!info.streamingData
|
!info.streamingData
|
||||||
@@ -358,22 +425,9 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* TODO: Options in prefs to set preferred video formats and adaptive streaming */
|
debug(`obtaining DASH itags for resolution: ${itagOpts.width}x${itagOpts.height}`);
|
||||||
const isAdaptiveEnabled = settings.get_boolean('yt-adaptive-enabled');
|
const dashItags = YTItags.getDashItags(itagOpts);
|
||||||
const allowedFormats = {
|
debug(`DASH itags: ${JSON.stringify(dashItags)}`);
|
||||||
video: [
|
|
||||||
133,
|
|
||||||
134,
|
|
||||||
135,
|
|
||||||
136,
|
|
||||||
137,
|
|
||||||
298,
|
|
||||||
299,
|
|
||||||
],
|
|
||||||
audio: [
|
|
||||||
140,
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredStreams = {
|
const filteredStreams = {
|
||||||
video: [],
|
video: [],
|
||||||
@@ -382,11 +436,11 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
|
|
||||||
for(let fmt of ['video', 'audio']) {
|
for(let fmt of ['video', 'audio']) {
|
||||||
debug(`filtering ${fmt} streams`);
|
debug(`filtering ${fmt} streams`);
|
||||||
let index = allowedFormats[fmt].length;
|
let index = dashItags[fmt].length;
|
||||||
|
|
||||||
while(index--) {
|
while(index--) {
|
||||||
const itag = allowedFormats[fmt][index];
|
const itag = dashItags[fmt][index];
|
||||||
const foundStream = info.streamingData.adaptiveFormats.find(stream => (stream.itag == itag));
|
const foundStream = info.streamingData.adaptiveFormats.find(stream => stream.itag == itag);
|
||||||
if(foundStream) {
|
if(foundStream) {
|
||||||
/* Parse and convert mimeType string into object */
|
/* Parse and convert mimeType string into object */
|
||||||
foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType);
|
foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType);
|
||||||
@@ -401,7 +455,7 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
filteredStreams[fmt].unshift(foundStream);
|
filteredStreams[fmt].unshift(foundStream);
|
||||||
debug(`added ${fmt} itag: ${foundStream.itag}`);
|
debug(`added ${fmt} itag: ${foundStream.itag}`);
|
||||||
|
|
||||||
if(!isAdaptiveEnabled)
|
if(!itagOpts.adaptive)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,8 +471,16 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
for(let stream of fmtArr) {
|
for(let stream of fmtArr) {
|
||||||
debug(`initial URL: ${stream.url}`);
|
debug(`initial URL: ${stream.url}`);
|
||||||
|
|
||||||
const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug);
|
/* Errors in some cases are to be expected here,
|
||||||
if(!result) return null;
|
* so be quiet about them and use fallback methods */
|
||||||
|
const result = await this._downloadDataPromise(
|
||||||
|
stream.url, 'HEAD'
|
||||||
|
).catch(err => debug(err.message));
|
||||||
|
|
||||||
|
if(!result || !result.uri) {
|
||||||
|
debug('redirect could not be resolved');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
stream.url = Misc.encodeHTML(result.uri)
|
stream.url = Misc.encodeHTML(result.uri)
|
||||||
.replace('?', '/')
|
.replace('?', '/')
|
||||||
@@ -440,16 +502,21 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getBestCombinedUri(info)
|
getBestCombinedUri(info, itagOpts)
|
||||||
{
|
{
|
||||||
debug('obtaining best combined URL');
|
debug(`obtaining best combined URL for resolution: ${itagOpts.width}x${itagOpts.height}`);
|
||||||
|
const streams = info.streamingData.formats;
|
||||||
|
|
||||||
if(!info.streamingData.formats.length)
|
if(!streams.length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const combinedStream = info.streamingData.formats[
|
const combinedItags = YTItags.getCombinedItags(itagOpts);
|
||||||
info.streamingData.formats.length - 1
|
let combinedStream = this.getBestStreamFromItags(streams, combinedItags);
|
||||||
];
|
|
||||||
|
if(!combinedStream) {
|
||||||
|
debug('trying any combined stream as last resort');
|
||||||
|
combinedStream = streams[streams.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
if(!combinedStream || !combinedStream.url)
|
if(!combinedStream || !combinedStream.url)
|
||||||
return null;
|
return null;
|
||||||
@@ -457,6 +524,23 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
return combinedStream.url;
|
return combinedStream.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBestStreamFromItags(streams, itags)
|
||||||
|
{
|
||||||
|
let index = itags.length;
|
||||||
|
|
||||||
|
while(index--) {
|
||||||
|
const itag = itags[index];
|
||||||
|
const stream = streams.find(stream => stream.itag == itag);
|
||||||
|
if(stream) {
|
||||||
|
debug(`found preferred stream itag: ${stream.itag}`);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug('could not find preferred stream for itags');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
compareLastVideoId(videoId)
|
compareLastVideoId(videoId)
|
||||||
{
|
{
|
||||||
if(!this.lastInfo)
|
if(!this.lastInfo)
|
||||||
@@ -506,7 +590,7 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
return resolve(result);
|
return resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(new Error(`response code: ${statusCode}`));
|
debug(`response code: ${statusCode}`);
|
||||||
|
|
||||||
/* Internal Soup codes mean download aborted
|
/* Internal Soup codes mean download aborted
|
||||||
* or some other error that cannot be handled
|
* or some other error that cannot be handled
|
||||||
@@ -694,14 +778,21 @@ var YouTubeClient = GObject.registerClass({
|
|||||||
|
|
||||||
_getIsCipher(data)
|
_getIsCipher(data)
|
||||||
{
|
{
|
||||||
/* Check only first best combined,
|
const stream = (data.formats.length)
|
||||||
* AFAIK there are no videos without it */
|
? data.formats[0]
|
||||||
if(data.formats[0].url)
|
: data.adaptiveFormats[0];
|
||||||
|
|
||||||
|
if(!stream) {
|
||||||
|
debug(new Error('no streams'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stream.url)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(
|
if(
|
||||||
data.formats[0].signatureCipher
|
stream.signatureCipher
|
||||||
|| data.formats[0].cipher
|
|| stream.cipher
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
80
src/youtubeItags.js
Normal file
80
src/youtubeItags.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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);
|
||||||
|
}
|
Reference in New Issue
Block a user