mirror of
https://github.com/Rafostar/clapper.git
synced 2025-09-02 01:12:02 +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.
|
||||
|
||||
## Other Questions?
|
||||
Feel free to ask me any questions.<br>
|
||||
Use either GitHub [discussions](https://github.com/Rafostar/clapper/discussions) or come and talk on Matrix: **#clapper-player:matrix.org**
|
||||
Feel free to ask me any questions. Come and talk on Matrix: [#clapper-player:matrix.org](https://matrix.to/#/#clapper-player:matrix.org)
|
||||
|
||||
## Special Thanks
|
||||
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.
|
||||
|
@@ -102,7 +102,11 @@
|
||||
<!-- YouTube -->
|
||||
<key name="yt-adaptive-enabled" type="b">
|
||||
<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>
|
||||
|
||||
<!-- Other -->
|
||||
|
@@ -52,6 +52,24 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<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">
|
||||
<description>
|
||||
<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)
|
||||
{
|
||||
MediaInfoUpdatedSignalData *data = g_new (MediaInfoUpdatedSignalData, 1);
|
||||
self->needs_info_update = FALSE;
|
||||
data->clapper = g_object_ref (self);
|
||||
g_mutex_lock (&self->lock);
|
||||
self->needs_info_update = FALSE;
|
||||
data->info = gst_clapper_media_info_copy (self->media_info);
|
||||
g_mutex_unlock (&self->lock);
|
||||
|
||||
@@ -946,9 +946,12 @@ change_state (GstClapper * self, GstClapperState state)
|
||||
gst_clapper_state_get_name (state));
|
||||
self->app_state = state;
|
||||
|
||||
if (state == GST_CLAPPER_STATE_STOPPED && self->rate != 1.0) {
|
||||
self->rate = 1.0;
|
||||
emit_rate_notify (self);
|
||||
if (state == GST_CLAPPER_STATE_STOPPED) {
|
||||
self->needs_info_update = FALSE;
|
||||
if (self->rate != 1.0) {
|
||||
self->rate = 1.0;
|
||||
emit_rate_notify (self);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
info =
|
||||
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);
|
||||
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);
|
||||
|
||||
if (emit_update)
|
||||
|
@@ -151,13 +151,9 @@ gtk_clapper_gl_widget_size_allocate (GtkWidget * widget,
|
||||
GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (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_height = height * scale_factor;
|
||||
|
||||
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_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;
|
||||
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.
|
||||
GTK calls render with GDK context already active. */
|
||||
@@ -672,7 +668,7 @@ done:
|
||||
if (priv->other_context)
|
||||
gst_gl_context_activate (priv->other_context, FALSE);
|
||||
|
||||
GTK_CLAPPER_GL_WIDGET_UNLOCK (widget);
|
||||
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
project('com.github.rafostar.Clapper', 'c', 'cpp',
|
||||
version: '0.2.0',
|
||||
version: '0.2.1',
|
||||
meson_version: '>= 0.50.0',
|
||||
license: 'GPL3',
|
||||
default_options: [
|
||||
|
@@ -2,7 +2,7 @@ Format: 3.0 (quilt)
|
||||
Source: clapper
|
||||
Binary: clapper
|
||||
Architecture: any
|
||||
Version: 0.2.0
|
||||
Version: 0.2.1
|
||||
Maintainer: Rafostar <rafostar.github@gmail.com>
|
||||
Build-Depends: debhelper (>= 10),
|
||||
meson (>= 0.50),
|
||||
|
@@ -1,5 +1,5 @@
|
||||
clapper (0.2.0) unstable; urgency=low
|
||||
clapper (0.2.1) unstable; urgency=low
|
||||
|
||||
* 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
|
||||
|
||||
Name: clapper
|
||||
Version: 0.2.0
|
||||
Version: 0.2.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Simple and modern GNOME media player
|
||||
|
||||
@@ -126,6 +126,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
|
||||
%{_libdir}/%{appname}/
|
||||
|
||||
%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
|
||||
- New version
|
||||
|
||||
|
4
src/controls.js
vendored
4
src/controls.js
vendored
@@ -18,6 +18,8 @@ class ClapperControls extends Gtk.Box
|
||||
can_focus: false,
|
||||
});
|
||||
|
||||
this.minFullViewWidth = 560;
|
||||
|
||||
this.currentPosition = 0;
|
||||
this.currentDuration = 0;
|
||||
this.isPositionDragging = false;
|
||||
@@ -473,7 +475,7 @@ class ClapperControls extends Gtk.Box
|
||||
|
||||
_onPlayerResize(width, height)
|
||||
{
|
||||
const isMobile = (width < 560);
|
||||
const isMobile = (width < this.minFullViewWidth);
|
||||
if(this.isMobile === isMobile)
|
||||
return;
|
||||
|
||||
|
@@ -228,6 +228,10 @@ class ClapperPrefsDialog extends Gtk.Dialog
|
||||
{
|
||||
title: 'Network',
|
||||
widget: Prefs.NetworkPage,
|
||||
},
|
||||
{
|
||||
title: 'YouTube',
|
||||
widget: Prefs.YouTubePage,
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -151,7 +151,7 @@ class ClapperHeaderBarBase extends Gtk.Box
|
||||
|
||||
for(let name of layoutArr) {
|
||||
/* Menu might be named "appmenu" */
|
||||
if(!menuAdded && (!name || name === 'appmenu'))
|
||||
if(!menuAdded && (!name || name === 'appmenu' || name === 'icon'))
|
||||
name = 'menu';
|
||||
|
||||
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 Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const YouTube = imports.src.youtube;
|
||||
const { PlayerBase } = imports.src.playerBase;
|
||||
const { PlaylistWidget } = imports.src.playlist;
|
||||
const { WebApp } = imports.src.webApp;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
let WebServer;
|
||||
|
||||
var Player = GObject.registerClass(
|
||||
class ClapperPlayer extends PlayerBase
|
||||
class ClapperPlayer extends GstClapper.Clapper
|
||||
{
|
||||
_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.needsFastSeekRestore = false;
|
||||
this.customVideoTitle = null;
|
||||
|
||||
this.windowMapped = false;
|
||||
this.canAutoFullscreen = false;
|
||||
this.playOnFullscreen = false;
|
||||
this.quitOnStop = false;
|
||||
@@ -32,15 +58,93 @@ class ClapperPlayer extends PlayerBase
|
||||
keyController.connect('key-released', this._onWidgetKeyReleased.bind(this));
|
||||
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('uri-loaded', this._onUriLoaded.bind(this));
|
||||
this.connect('end-of-stream', this._onStreamEnded.bind(this));
|
||||
this.connect('warning', this._onPlayerWarning.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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
this.customVideoTitle = null;
|
||||
@@ -54,7 +158,11 @@ class ClapperPlayer extends PlayerBase
|
||||
if(!this.ytClient)
|
||||
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 => {
|
||||
this.customVideoTitle = data.title;
|
||||
super.set_uri(data.uri);
|
||||
@@ -121,10 +229,9 @@ class ClapperPlayer extends PlayerBase
|
||||
this.playlistWidget.addItem(uri);
|
||||
}
|
||||
|
||||
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
||||
if(!firstTrack) return;
|
||||
|
||||
firstTrack.activate();
|
||||
/* If not mapped yet, first track will play after map */
|
||||
if(this.windowMapped)
|
||||
this._playFirstTrack();
|
||||
}
|
||||
|
||||
set_subtitles(source)
|
||||
@@ -179,7 +286,7 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
seek_seconds(seconds)
|
||||
{
|
||||
this.seek(seconds * 1000000000);
|
||||
this.seek(seconds * Gst.SECOND);
|
||||
}
|
||||
|
||||
seek_chapter(seconds)
|
||||
@@ -189,12 +296,9 @@ class ClapperPlayer extends PlayerBase
|
||||
return;
|
||||
}
|
||||
|
||||
/* FIXME: Remove this check when GstPlay(er) have set_seek_mode function */
|
||||
if(this.set_seek_mode) {
|
||||
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
||||
this.seekingMode = 'normal';
|
||||
this.needsFastSeekRestore = true;
|
||||
}
|
||||
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
|
||||
this.seekingMode = 'normal';
|
||||
this.needsFastSeekRestore = true;
|
||||
|
||||
this.seek_seconds(seconds);
|
||||
}
|
||||
@@ -251,6 +355,14 @@ class ClapperPlayer extends PlayerBase
|
||||
this[action]();
|
||||
}
|
||||
|
||||
emitWs(action, value)
|
||||
{
|
||||
if(!this.webserver)
|
||||
return;
|
||||
|
||||
this.webserver.sendMessage({ action, value });
|
||||
}
|
||||
|
||||
receiveWs(action, value)
|
||||
{
|
||||
switch(action) {
|
||||
@@ -275,7 +387,7 @@ class ClapperPlayer extends PlayerBase
|
||||
clapperWidget.toggleFullscreen();
|
||||
break;
|
||||
default:
|
||||
super.receiveWs(action, value);
|
||||
debug(`unhandled WebSocket action: ${action}`);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -291,6 +403,14 @@ class ClapperPlayer extends PlayerBase
|
||||
: Gst.filename_to_uri(source);
|
||||
}
|
||||
|
||||
_playFirstTrack()
|
||||
{
|
||||
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
||||
if(!firstTrack) return;
|
||||
|
||||
firstTrack.activate();
|
||||
}
|
||||
|
||||
_performCloseCleanup(window)
|
||||
{
|
||||
window.disconnect(this.closeRequestSignal);
|
||||
@@ -312,8 +432,8 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
let resumeInfo = {};
|
||||
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
|
||||
const resumeTime = Math.floor(this.position / 1000000000);
|
||||
const resumeDuration = this.duration / 1000000000;
|
||||
const resumeTime = Math.floor(this.position / Gst.SECOND);
|
||||
const resumeDuration = this.duration / Gst.SECOND;
|
||||
|
||||
/* Do not save resume info when video is very short,
|
||||
* just started or almost finished */
|
||||
@@ -522,6 +642,12 @@ class ClapperPlayer extends PlayerBase
|
||||
}
|
||||
}
|
||||
|
||||
_onWindowMap(window)
|
||||
{
|
||||
this.windowMapped = true;
|
||||
this._playFirstTrack();
|
||||
}
|
||||
|
||||
_onCloseRequest(window)
|
||||
{
|
||||
this._performCloseCleanup(window);
|
||||
@@ -532,4 +658,109 @@ class ClapperPlayer extends PlayerBase
|
||||
this.quitOnStop = true;
|
||||
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.filename = filename || uri;
|
||||
this.set_tooltip_text(this.filename);
|
||||
|
||||
const box = new Gtk.Box({
|
||||
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(
|
||||
class ClapperGStreamerPage extends PrefsBase.Grid
|
||||
{
|
||||
|
@@ -370,6 +370,18 @@ class ClapperButtonsRevealer extends Gtk.Revealer
|
||||
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)
|
||||
{
|
||||
const cssClass = 'halfrotate';
|
||||
@@ -388,7 +400,8 @@ class ClapperButtonsRevealer extends Gtk.Revealer
|
||||
|
||||
_onRevealChild(button)
|
||||
{
|
||||
this._setRotateClass(button.child, true);
|
||||
if(this.reveal_child !== this.child_revealed)
|
||||
this._setRotateClass(button.child, true);
|
||||
}
|
||||
|
||||
_onChildRevealed(button)
|
||||
|
@@ -371,7 +371,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = Math.floor(start / 1000000) / 1000;
|
||||
const pos = Math.floor(start / Gst.MSECOND) / 1000;
|
||||
const tags = subentry.get_tags();
|
||||
|
||||
this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null);
|
||||
@@ -467,7 +467,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
_onPlayerDurationChanged(player, duration)
|
||||
{
|
||||
const durationSeconds = duration / 1000000000;
|
||||
const durationSeconds = duration / Gst.SECOND;
|
||||
const durationFloor = Math.floor(durationSeconds);
|
||||
|
||||
/* Sometimes GstPlayer might re-emit
|
||||
@@ -515,7 +515,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
)
|
||||
return;
|
||||
|
||||
const positionSeconds = Math.round(position / 1000000000);
|
||||
const positionSeconds = Math.round(position / Gst.SECOND);
|
||||
if(positionSeconds === this.controls.currentPosition)
|
||||
return;
|
||||
|
||||
@@ -541,6 +541,12 @@ class ClapperWidget extends Gtk.Grid
|
||||
if(width === this.layoutWidth)
|
||||
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;
|
||||
|
||||
if(this.isFullscreenMode)
|
||||
@@ -572,6 +578,8 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
surface.connect('notify::state', this._onStateNotify.bind(this));
|
||||
surface.connect('layout', this._onLayoutUpdate.bind(this));
|
||||
|
||||
this.player._onWindowMap(window);
|
||||
}
|
||||
|
||||
_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 FileOps = imports.src.fileOps;
|
||||
const Misc = imports.src.misc;
|
||||
const YTItags = imports.src.youtubeItags;
|
||||
const YTDL = imports.src.assets['node-ytdl-core'];
|
||||
|
||||
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);
|
||||
|
||||
@@ -312,28 +313,40 @@ var YouTubeClient = GObject.registerClass({
|
||||
throw new Error('no YouTube video info');
|
||||
|
||||
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) {
|
||||
debug('parsed video info to dash info');
|
||||
const dash = Dash.generateDash(dashInfo);
|
||||
uri = await this.getHLSUriAsync(info, itagOpts);
|
||||
|
||||
if(dash) {
|
||||
debug('got dash data');
|
||||
if(!uri) {
|
||||
const dashInfo = await this.getDashInfoAsync(info, itagOpts).catch(debug);
|
||||
|
||||
const dashFile = await FileOps.saveFilePromise(
|
||||
'tmp', null, 'clapper.mpd', dash
|
||||
).catch(debug);
|
||||
if(dashInfo) {
|
||||
debug('parsed video info to dash info');
|
||||
const dash = Dash.generateDash(dashInfo);
|
||||
|
||||
if(dashFile)
|
||||
uri = dashFile.get_uri();
|
||||
if(dash) {
|
||||
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)
|
||||
uri = this.getBestCombinedUri(info);
|
||||
uri = this.getBestCombinedUri(info, itagOpts);
|
||||
|
||||
if(!uri)
|
||||
throw new Error('no YouTube video URI');
|
||||
@@ -349,7 +362,61 @@ var YouTubeClient = GObject.registerClass({
|
||||
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(
|
||||
!info.streamingData
|
||||
@@ -358,22 +425,9 @@ var YouTubeClient = GObject.registerClass({
|
||||
)
|
||||
return null;
|
||||
|
||||
/* TODO: Options in prefs to set preferred video formats and adaptive streaming */
|
||||
const isAdaptiveEnabled = settings.get_boolean('yt-adaptive-enabled');
|
||||
const allowedFormats = {
|
||||
video: [
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
298,
|
||||
299,
|
||||
],
|
||||
audio: [
|
||||
140,
|
||||
]
|
||||
};
|
||||
debug(`obtaining DASH itags for resolution: ${itagOpts.width}x${itagOpts.height}`);
|
||||
const dashItags = YTItags.getDashItags(itagOpts);
|
||||
debug(`DASH itags: ${JSON.stringify(dashItags)}`);
|
||||
|
||||
const filteredStreams = {
|
||||
video: [],
|
||||
@@ -382,11 +436,11 @@ var YouTubeClient = GObject.registerClass({
|
||||
|
||||
for(let fmt of ['video', 'audio']) {
|
||||
debug(`filtering ${fmt} streams`);
|
||||
let index = allowedFormats[fmt].length;
|
||||
let index = dashItags[fmt].length;
|
||||
|
||||
while(index--) {
|
||||
const itag = allowedFormats[fmt][index];
|
||||
const foundStream = info.streamingData.adaptiveFormats.find(stream => (stream.itag == itag));
|
||||
const itag = dashItags[fmt][index];
|
||||
const foundStream = info.streamingData.adaptiveFormats.find(stream => stream.itag == itag);
|
||||
if(foundStream) {
|
||||
/* Parse and convert mimeType string into object */
|
||||
foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType);
|
||||
@@ -401,7 +455,7 @@ var YouTubeClient = GObject.registerClass({
|
||||
filteredStreams[fmt].unshift(foundStream);
|
||||
debug(`added ${fmt} itag: ${foundStream.itag}`);
|
||||
|
||||
if(!isAdaptiveEnabled)
|
||||
if(!itagOpts.adaptive)
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -417,8 +471,16 @@ var YouTubeClient = GObject.registerClass({
|
||||
for(let stream of fmtArr) {
|
||||
debug(`initial URL: ${stream.url}`);
|
||||
|
||||
const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug);
|
||||
if(!result) return null;
|
||||
/* Errors in some cases are to be expected here,
|
||||
* 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)
|
||||
.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;
|
||||
|
||||
const combinedStream = info.streamingData.formats[
|
||||
info.streamingData.formats.length - 1
|
||||
];
|
||||
const combinedItags = YTItags.getCombinedItags(itagOpts);
|
||||
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)
|
||||
return null;
|
||||
@@ -457,6 +524,23 @@ var YouTubeClient = GObject.registerClass({
|
||||
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)
|
||||
{
|
||||
if(!this.lastInfo)
|
||||
@@ -506,7 +590,7 @@ var YouTubeClient = GObject.registerClass({
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
debug(new Error(`response code: ${statusCode}`));
|
||||
debug(`response code: ${statusCode}`);
|
||||
|
||||
/* Internal Soup codes mean download aborted
|
||||
* or some other error that cannot be handled
|
||||
@@ -694,14 +778,21 @@ var YouTubeClient = GObject.registerClass({
|
||||
|
||||
_getIsCipher(data)
|
||||
{
|
||||
/* Check only first best combined,
|
||||
* AFAIK there are no videos without it */
|
||||
if(data.formats[0].url)
|
||||
const stream = (data.formats.length)
|
||||
? data.formats[0]
|
||||
: data.adaptiveFormats[0];
|
||||
|
||||
if(!stream) {
|
||||
debug(new Error('no streams'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(stream.url)
|
||||
return false;
|
||||
|
||||
if(
|
||||
data.formats[0].signatureCipher
|
||||
|| data.formats[0].cipher
|
||||
stream.signatureCipher
|
||||
|| stream.cipher
|
||||
)
|
||||
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