From 62573d3a88f18c6edc4dbd265a34fc65bc26be3d Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 14:55:50 +0100 Subject: [PATCH 01/26] Move main.js to source files dir --- bin/com.github.rafostar.Clapper.in | 2 +- main.js => clapper_src/main.js | 0 meson.build | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) rename main.js => clapper_src/main.js (100%) diff --git a/bin/com.github.rafostar.Clapper.in b/bin/com.github.rafostar.Clapper.in index b62c9a2d..78cd7495 100644 --- a/bin/com.github.rafostar.Clapper.in +++ b/bin/com.github.rafostar.Clapper.in @@ -9,4 +9,4 @@ Package.init({ libdir: '@libdir@', datadir: '@datadir@', }); -Package.run(imports.main); +Package.run(imports.clapper_src.main); diff --git a/main.js b/clapper_src/main.js similarity index 100% rename from main.js rename to clapper_src/main.js diff --git a/meson.build b/meson.build index beee3093..52327a32 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,5 @@ installdir = join_paths(get_option('prefix'), 'share', meson.project_name()) install_subdir('clapper_src', install_dir : installdir) install_subdir('css', install_dir : installdir) install_subdir('ui', install_dir : installdir) -install_data('main.js', install_dir : installdir) meson.add_install_script('build-aux/meson/postinstall.py') From 083445a830cef0a7385230c1ce171481dd75486f Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 15:22:35 +0100 Subject: [PATCH 02/26] Split header bar source file into two This allows creating another headerbar with different functionality from the same source code. --- clapper_src/headerbar.js | 108 ++--------------------------- clapper_src/headerbarBase.js | 129 +++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 103 deletions(-) create mode 100644 clapper_src/headerbarBase.js diff --git a/clapper_src/headerbar.js b/clapper_src/headerbar.js index 9060b80b..c300b9ba 100644 --- a/clapper_src/headerbar.js +++ b/clapper_src/headerbar.js @@ -1,94 +1,17 @@ -const { GObject, Gtk, Pango } = imports.gi; +const { GObject } = imports.gi; +const { HeaderBarBase } = imports.clapper_src.headerbarBase; var HeaderBar = GObject.registerClass( -class ClapperHeaderBar extends Gtk.HeaderBar +class ClapperHeaderBar extends HeaderBarBase { - _init(window, models) + _init(window) { - super._init({ - can_focus: false, - }); - this.add_css_class('noborder'); + super._init(window); - this.set_title_widget(this._createWidgetForWindow(window)); let clapperWidget = window.get_child(); - - let addMediaButton = new Gtk.MenuButton({ - icon_name: 'list-add-symbolic', - }); - let addMediaPopover = new HeaderBarPopover(models.addMediaMenu); - addMediaButton.set_popover(addMediaPopover); - this.pack_start(addMediaButton); - - let openMenuButton = new Gtk.MenuButton({ - icon_name: 'open-menu-symbolic', - }); - let settingsPopover = new HeaderBarPopover(models.settingsMenu); - openMenuButton.set_popover(settingsPopover); - this.pack_end(openMenuButton); - - let buttonsBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - }); - buttonsBox.add_css_class('linked'); - - let floatButton = new Gtk.Button({ - icon_name: 'preferences-desktop-remote-desktop-symbolic', - }); - floatButton.connect('clicked', this._onFloatButtonClicked.bind(this)); clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible', GObject.BindingFlags.INVERT_BOOLEAN ); - buttonsBox.append(floatButton); - - let fullscreenButton = new Gtk.Button({ - icon_name: 'view-fullscreen-symbolic', - }); - fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this)); - - buttonsBox.append(fullscreenButton); - this.pack_end(buttonsBox); - } - - updateHeaderBar(title, subtitle) - { - this.titleLabel.label = title; - this.subtitleLabel.visible = (subtitle !== null); - - if(subtitle) - this.subtitleLabel.label = subtitle; - } - - _createWidgetForWindow(window) - { - let box = new Gtk.Box ({ - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.CENTER, - }); - - this.titleLabel = new Gtk.Label({ - halign: Gtk.Align.CENTER, - single_line_mode: true, - ellipsize: Pango.EllipsizeMode.END, - width_chars: 5, - }); - this.titleLabel.add_css_class('title'); - this.titleLabel.set_parent(box); - - window.bind_property('title', this.titleLabel, 'label', - GObject.BindingFlags.SYNC_CREATE - ); - - this.subtitleLabel = new Gtk.Label({ - halign: Gtk.Align.CENTER, - single_line_mode: true, - ellipsize: Pango.EllipsizeMode.END, - }); - this.subtitleLabel.add_css_class('subtitle'); - this.subtitleLabel.set_parent(box); - this.subtitleLabel.visible = false; - - return box; } _onFloatButtonClicked() @@ -103,24 +26,3 @@ class ClapperHeaderBar extends Gtk.HeaderBar window.fullscreen(); } }); - -var HeaderBarPopover = GObject.registerClass( -class ClapperHeaderBarPopover extends Gtk.PopoverMenu -{ - _init(model) - { - super._init({ - menu_model: model, - }); - - this.connect('closed', this._onClosed.bind(this)); - } - - _onClosed() - { - let root = this.get_root(); - let clapperWidget = root.get_child(); - - clapperWidget.player.widget.grab_focus(); - } -}); diff --git a/clapper_src/headerbarBase.js b/clapper_src/headerbarBase.js new file mode 100644 index 00000000..09d32b40 --- /dev/null +++ b/clapper_src/headerbarBase.js @@ -0,0 +1,129 @@ +const { GObject, Gtk, Pango } = imports.gi; +const Misc = imports.clapper_src.misc; + +var HeaderBarBase = GObject.registerClass( +class ClapperHeaderBarBase extends Gtk.HeaderBar +{ + _init(window) + { + super._init({ + can_focus: false, + }); + + let clapperPath = Misc.getClapperPath(); + let uiBuilder = Gtk.Builder.new_from_file( + `${clapperPath}/ui/clapper.ui` + ); + let models = { + addMediaMenu: uiBuilder.get_object('addMediaMenu'), + settingsMenu: uiBuilder.get_object('settingsMenu'), + }; + + this.add_css_class('noborder'); + this.set_title_widget(this._createWidgetForWindow(window)); + + let addMediaButton = new Gtk.MenuButton({ + icon_name: 'list-add-symbolic', + }); + let addMediaPopover = new HeaderBarPopover(models.addMediaMenu); + addMediaButton.set_popover(addMediaPopover); + this.pack_start(addMediaButton); + + let openMenuButton = new Gtk.MenuButton({ + icon_name: 'open-menu-symbolic', + }); + let settingsPopover = new HeaderBarPopover(models.settingsMenu); + openMenuButton.set_popover(settingsPopover); + this.pack_end(openMenuButton); + + let buttonsBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + }); + buttonsBox.add_css_class('linked'); + + let floatButton = new Gtk.Button({ + icon_name: 'preferences-desktop-remote-desktop-symbolic', + }); + floatButton.connect('clicked', this._onFloatButtonClicked.bind(this)); + buttonsBox.append(floatButton); + + let fullscreenButton = new Gtk.Button({ + icon_name: 'view-fullscreen-symbolic', + }); + fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this)); + + buttonsBox.append(fullscreenButton); + this.pack_end(buttonsBox); + } + + updateHeaderBar(title, subtitle) + { + this.titleLabel.label = title; + this.subtitleLabel.visible = (subtitle !== null); + + if(subtitle) + this.subtitleLabel.label = subtitle; + } + + _createWidgetForWindow(window) + { + let box = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.CENTER, + }); + + this.titleLabel = new Gtk.Label({ + halign: Gtk.Align.CENTER, + single_line_mode: true, + ellipsize: Pango.EllipsizeMode.END, + width_chars: 5, + }); + this.titleLabel.add_css_class('title'); + this.titleLabel.set_parent(box); + + window.bind_property('title', this.titleLabel, 'label', + GObject.BindingFlags.SYNC_CREATE + ); + + this.subtitleLabel = new Gtk.Label({ + halign: Gtk.Align.CENTER, + single_line_mode: true, + ellipsize: Pango.EllipsizeMode.END, + }); + this.subtitleLabel.add_css_class('subtitle'); + this.subtitleLabel.set_parent(box); + this.subtitleLabel.visible = false; + + return box; + } + + _onFloatButtonClicked() + { + } + + _onFullscreenButtonClicked() + { + } +}); + +var HeaderBarPopover = GObject.registerClass( +class ClapperHeaderBarPopover extends Gtk.PopoverMenu +{ + _init(model) + { + super._init({ + menu_model: model, + }); + + this.connect('closed', this._onClosed.bind(this)); + } + + _onClosed() + { + let root = this.get_root(); + let clapperWidget = root.get_child(); + + if(clapperWidget && clapperWidget.player) + clapperWidget.player.widget.grab_focus(); + } +}); From 6315669933b2e5e806f80bae3129a9f798609bd0 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 15:32:05 +0100 Subject: [PATCH 03/26] Split app source file into two This allows creating different app from the same source code. --- clapper_src/app.js | 119 ++++------------------------------------- clapper_src/appBase.js | 112 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 108 deletions(-) create mode 100644 clapper_src/appBase.js diff --git a/clapper_src/app.js b/clapper_src/app.js index c7d38900..575b3808 100644 --- a/clapper_src/app.js +++ b/clapper_src/app.js @@ -1,136 +1,39 @@ -const { Gio, GObject, Gtk } = imports.gi; +const { GObject } = imports.gi; +const { AppBase } = imports.clapper_src.appBase; const { HeaderBar } = imports.clapper_src.headerbar; const { Widget } = imports.clapper_src.widget; const Debug = imports.clapper_src.debug; -const Menu = imports.clapper_src.menu; -const Misc = imports.clapper_src.misc; let { debug } = Debug; -let { settings } = Misc; var App = GObject.registerClass( -class ClapperApp extends Gtk.Application +class ClapperApp extends AppBase { - _init(opts) - { - super._init({ - application_id: Misc.appId - }); - - let defaults = { - playlist: [], - }; - Object.assign(this, defaults, opts); - - this.doneFirstActivate = false; - } - vfunc_startup() { super.vfunc_startup(); - let window = new Gtk.ApplicationWindow({ - application: this, - title: Misc.appName, - }); - window.isClapperApp = true; - window.add_css_class('nobackground'); - - if(!settings.get_boolean('render-shadows')) - window.add_css_class('gpufriendly'); - - if( - settings.get_boolean('dark-theme') - && settings.get_boolean('brighter-sliders') - ) - window.add_css_class('brightscale'); - - for(let action in Menu.actions) { - let simpleAction = new Gio.SimpleAction({ - name: action - }); - simpleAction.connect( - 'activate', () => Menu.actions[action](this.active_window) - ); - this.add_action(simpleAction); - } + this.active_window.isClapperApp = true; + this.active_window.add_css_class('nobackground'); let clapperWidget = new Widget(); - window.set_child(clapperWidget); + this.active_window.set_child(clapperWidget); + + let headerBar = new HeaderBar(this.active_window); + this.active_window.set_titlebar(headerBar); let size = clapperWidget.windowSize; - window.set_default_size(size[0], size[1]); + this.active_window.set_default_size(size[0], size[1]); debug(`restored window size: ${size[0]}x${size[1]}`); - - let clapperPath = Misc.getClapperPath(); - let uiBuilder = Gtk.Builder.new_from_file( - `${clapperPath}/ui/clapper.ui` - ); - let models = { - addMediaMenu: uiBuilder.get_object('addMediaMenu'), - settingsMenu: uiBuilder.get_object('settingsMenu'), - }; - let headerBar = new HeaderBar(window, models); - window.set_titlebar(headerBar); - } - - vfunc_activate() - { - super.vfunc_activate(); - - if(!this.doneFirstActivate) - this._onFirstActivate(); - - this.active_window.present(); - } - - run(arr) - { - super.run(arr || []); - } - - _onFirstActivate() - { - let gtkSettings = Gtk.Settings.get_default(); - settings.bind( - 'dark-theme', gtkSettings, - 'gtk-application-prefer-dark-theme', - Gio.SettingsBindFlags.GET - ); - this._onThemeChanged(gtkSettings); - gtkSettings.connect('notify::gtk-theme-name', this._onThemeChanged.bind(this)); - - this.windowShowSignal = this.active_window.connect( - 'show', this._onWindowShow.bind(this) - ); - - this.doneFirstActivate = true; } _onWindowShow(window) { - window.disconnect(this.windowShowSignal); - this.windowShowSignal = null; + super._onWindowShow(window); if(this.playlist.length) { let { player } = window.get_child(); player.set_playlist(this.playlist); } } - - _onThemeChanged(gtkSettings) - { - const theme = gtkSettings.gtk_theme_name; - debug(`user selected theme: ${theme}`); - - if(!theme.endsWith('-dark')) - return; - - /* We need to request a default theme with optional dark variant - to make the "gtk_application_prefer_dark_theme" setting work */ - const parsedTheme = theme.substring(0, theme.lastIndexOf('-')); - - gtkSettings.gtk_theme_name = parsedTheme; - debug(`set theme: ${parsedTheme}`); - } }); diff --git a/clapper_src/appBase.js b/clapper_src/appBase.js new file mode 100644 index 00000000..b2224612 --- /dev/null +++ b/clapper_src/appBase.js @@ -0,0 +1,112 @@ +const { Gio, GObject, Gtk } = imports.gi; +const Debug = imports.clapper_src.debug; +const Menu = imports.clapper_src.menu; +const Misc = imports.clapper_src.misc; + +let { debug } = Debug; +let { settings } = Misc; + +var AppBase = GObject.registerClass( +class ClapperAppBase extends Gtk.Application +{ + _init(opts) + { + opts = opts || {}; + + let defaults = { + idPostfix: '', + playlist: [], + }; + Object.assign(this, defaults, opts); + + super._init({ + application_id: Misc.appId + this.idPostfix + }); + + this.doneFirstActivate = false; + } + + vfunc_startup() + { + super.vfunc_startup(); + + let window = new Gtk.ApplicationWindow({ + application: this, + title: Misc.appName, + }); + + if(!settings.get_boolean('render-shadows')) + window.add_css_class('gpufriendly'); + + if( + settings.get_boolean('dark-theme') + && settings.get_boolean('brighter-sliders') + ) + window.add_css_class('brightscale'); + + for(let action in Menu.actions) { + let simpleAction = new Gio.SimpleAction({ + name: action + }); + simpleAction.connect( + 'activate', () => Menu.actions[action](this.active_window) + ); + this.add_action(simpleAction); + } + } + + vfunc_activate() + { + super.vfunc_activate(); + + if(!this.doneFirstActivate) + this._onFirstActivate(); + + this.active_window.present(); + } + + run(arr) + { + super.run(arr || []); + } + + _onFirstActivate() + { + let gtkSettings = Gtk.Settings.get_default(); + settings.bind( + 'dark-theme', gtkSettings, + 'gtk-application-prefer-dark-theme', + Gio.SettingsBindFlags.GET + ); + this._onThemeChanged(gtkSettings); + gtkSettings.connect('notify::gtk-theme-name', this._onThemeChanged.bind(this)); + + this.windowShowSignal = this.active_window.connect( + 'show', this._onWindowShow.bind(this) + ); + + this.doneFirstActivate = true; + } + + _onWindowShow(window) + { + window.disconnect(this.windowShowSignal); + this.windowShowSignal = null; + } + + _onThemeChanged(gtkSettings) + { + const theme = gtkSettings.gtk_theme_name; + debug(`user selected theme: ${theme}`); + + if(!theme.endsWith('-dark')) + return; + + /* We need to request a default theme with optional dark variant + to make the "gtk_application_prefer_dark_theme" setting work */ + const parsedTheme = theme.substring(0, theme.lastIndexOf('-')); + + gtkSettings.gtk_theme_name = parsedTheme; + debug(`set theme: ${parsedTheme}`); + } +}); From 4875a31be4521674d1e3a06d05b324da5274adcf Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 15:38:25 +0100 Subject: [PATCH 04/26] Add initial ClapperRemote app --- bin/com.github.rafostar.ClapperRemote.in | 12 ++++++++++++ bin/meson.build | 20 ++++++++++++-------- clapper_src/appRemote.js | 17 +++++++++++++++++ clapper_src/mainRemote.js | 17 +++++++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 bin/com.github.rafostar.ClapperRemote.in create mode 100644 clapper_src/appRemote.js create mode 100644 clapper_src/mainRemote.js diff --git a/bin/com.github.rafostar.ClapperRemote.in b/bin/com.github.rafostar.ClapperRemote.in new file mode 100644 index 00000000..e567a7b9 --- /dev/null +++ b/bin/com.github.rafostar.ClapperRemote.in @@ -0,0 +1,12 @@ +#!@GJS@ + +const Package = imports.package; + +Package.init({ + name: '@PACKAGE_NAME@', + version: '@PACKAGE_VERSION@', + prefix: '@prefix@', + libdir: '@libdir@', + datadir: '@datadir@', +}); +Package.run(imports.clapper_src.mainRemote); diff --git a/bin/meson.build b/bin/meson.build index 2cdf501d..6a3aec8e 100644 --- a/bin/meson.build +++ b/bin/meson.build @@ -6,11 +6,15 @@ bin_conf.set('prefix', get_option('prefix')) bin_conf.set('libdir', join_paths(get_option('prefix'), get_option('libdir'))) bin_conf.set('datadir', join_paths(get_option('prefix'), get_option('datadir'))) -configure_file( - input: 'com.github.rafostar.Clapper.in', - output: 'com.github.rafostar.Clapper', - configuration: bin_conf, - install: true, - install_dir: get_option('bindir'), - install_mode: 'rwxr-xr-x' -) +clapper_apps = [ '', 'Remote' ] + +foreach id_postfix : clapper_apps + configure_file( + input: 'com.github.rafostar.Clapper' + id_postfix + '.in', + output: 'com.github.rafostar.Clapper' + id_postfix, + configuration: bin_conf, + install: true, + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x' + ) +endforeach diff --git a/clapper_src/appRemote.js b/clapper_src/appRemote.js new file mode 100644 index 00000000..ae34f9c2 --- /dev/null +++ b/clapper_src/appRemote.js @@ -0,0 +1,17 @@ +const { GObject } = imports.gi; +const { AppBase } = imports.clapper_src.appBase; +const { HeaderBarBase } = imports.clapper_src.headerbarBase; + +var AppRemote = GObject.registerClass( +class ClapperAppRemote extends AppBase +{ + vfunc_startup() + { + super.vfunc_startup(); + + let headerBar = new HeaderBarBase(this.active_window); + this.active_window.set_titlebar(headerBar); + + this.active_window.maximize(); + } +}); diff --git a/clapper_src/mainRemote.js b/clapper_src/mainRemote.js new file mode 100644 index 00000000..6321f769 --- /dev/null +++ b/clapper_src/mainRemote.js @@ -0,0 +1,17 @@ +imports.gi.versions.Gdk = '4.0'; +imports.gi.versions.Gtk = '4.0'; + +const { AppRemote } = imports.clapper_src.appRemote; +const Misc = imports.clapper_src.misc; + +const opts = { + idPostfix: 'Remote', +}; + +Misc.clapperPath = pkg.datadir + '/' + + pkg.name.substring(0, pkg.name.lastIndexOf(opts.idPostfix)); + +function main() +{ + new AppRemote(opts).run(); +} From 26f8b6994eedcb668475b931469d79d3329e1650 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 22:06:00 +0100 Subject: [PATCH 05/26] Add WebSocket server --- clapper_src/webserver.js | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 clapper_src/webserver.js diff --git a/clapper_src/webserver.js b/clapper_src/webserver.js new file mode 100644 index 00000000..2548003a --- /dev/null +++ b/clapper_src/webserver.js @@ -0,0 +1,158 @@ +const { Soup, GObject } = imports.gi; +const ByteArray = imports.byteArray; +const Debug = imports.clapper_src.debug; + +let { debug } = Debug; + +var WebServer = GObject.registerClass({ + Signals: { + 'websocket-data': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [ + GObject.TYPE_STRING, + GObject.TYPE_INT || GObject.TYPE_STRING + ] + } + } +}, class ClapperWebServer extends Soup.Server +{ + _init(port) + { + super._init(); + + this.isListening = false; + this.listeningPort = null; + this.wsConns = []; + + if(port) + this.setListeningPort(port); + } + + setListeningPort(port) + { + if(!port) + return; + + const wasListening = this.isListening; + + if(wasListening) + this.stopListening(); + + this.listeningPort = port; + + if(wasListening) + this.startListening(); + } + + startListening() + { + if(this.isListening || !this.listeningPort) + return; + + let isListening = false; + + this.add_handler('/', this._onDefaultAccess.bind(this)); + this.add_websocket_handler('/websocket', null, null, this._onWsConnection.bind(this)); + + try { + isListening = this.listen_local(this.listeningPort, Soup.ServerListenOptions.IPV4_ONLY); + } + catch(err) { + debug(err); + } + + if(isListening) { + const uris = this.get_uris(); + const usedPort = uris[0].get_port(); + debug(`WebSocket server listening on port: ${usedPort}`); + } + else { + debug(new Error('WebSocket server could not start listening')); + this._closeCleanup(); + } + + this.isListening = isListening; + } + + stopListening() + { + if(!this.isListening) + return; + + this._closeCleanup(); + this.disconnect(); + + this.isListening = false; + this.listeningPort = null; + } + + sendMessage(data) + { + for(const connection of this.wsConns) { + if(connection.state !== Soup.WebsocketState.OPEN) + continue; + + connection.send_text(JSON.stringify(data)); + } + } + + _closeCleanup() + { + while(this.wsConns.length) { + const connection = this.wsConns.pop(); + + if(connection.state !== Soup.WebsocketState.OPEN) + continue; + + connection.close(Soup.WebsocketCloseCode.NORMAL, null); + } + + this.remove_handler('/websocket'); + this.remove_handler('/'); + } + + _onWsConnection(server, connection) + { + debug('new WebSocket connection'); + + connection.connect('message', this._onWsMessage.bind(this)); + connection.connect('closed', this._onWsClosed.bind(this)); + + this.wsConns.push(connection); + debug(`total WebSocket connections: ${this.wsConns.length}`); + } + + _onWsMessage(connection, dataType, bytes) + { + if(dataType !== Soup.WebsocketDataType.TEXT) + return debug('ignoring non-text WebSocket message'); + + const msg = bytes.get_data(); + let parsedMsg = null; + + try { + parsedMsg = JSON.parse(ByteArray.toString(msg)); + } + catch(err) { + debug(err); + } + + if(!parsedMsg || !parsedMsg.action) + return debug('no "action" in parsed WebSocket message'); + + this.emit('websocket-data', parsedMsg.action, parsedMsg.value || 0); + } + + _onWsClosed(connection) + { + debug('closed WebSocket connection'); + + this.wsConns = this.wsConns.filter(conn => conn !== connection); + debug(`remaining WebSocket connections: ${this.wsConns.length}`); + } + + _onDefaultAccess(server, msg) + { + msg.status_code = 404; + } +}); From d5d5aa9bac05b46cc026d4f077359a8f3815e5b5 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Fri, 11 Dec 2020 23:38:49 +0100 Subject: [PATCH 06/26] Integrate basic web server functionality into player --- clapper_src/player.js | 16 +++++++++ clapper_src/playerBase.js | 34 ++++++++++++++++++++ data/com.github.rafostar.Clapper.gschema.xml | 10 ++++++ 3 files changed, 60 insertions(+) diff --git a/clapper_src/player.js b/clapper_src/player.js index 776ed62c..be06fddc 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -639,6 +639,22 @@ class ClapperPlayer extends PlayerBase return true; } + _onWsData(server, action, value) + { + switch(action) { + case 'toggle-play': + this.toggle_play(); + break; + case 'play': + case 'pause': + this[action](); + break; + default: + super._onWsData(server, action, value); + break; + } + } + _onCloseRequest(window) { this._performCloseCleanup(window); diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index d703ff33..ceabf855 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -2,6 +2,8 @@ const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; +let WebServer; + /* PlayFlags are not exported through GI */ Gst.PlayFlags = { VIDEO: 1, @@ -66,6 +68,9 @@ class ClapperPlayerBase extends GstPlayer.Player this.state = GstPlayer.PlayerState.STOPPED; this.visualization_enabled = false; + this.webserver = null; + this.websocketSignal = null; + this.set_all_plugins_ranks(); this.set_initial_config(); this.set_and_bind_settings(); @@ -89,6 +94,7 @@ class ClapperPlayerBase extends GstPlayer.Player 'audio-offset', 'subtitle-offset', 'play-flags', + 'webserver-enabled', ]; for(let key of settingsToSet) @@ -175,6 +181,11 @@ class ClapperPlayerBase extends GstPlayer.Player this.widget.queue_render(); } + _onWsData(server, action, value) + { + debug(`unhandled WebSocket action: ${action}`); + } + _onSettingsKeyChanged(settings, key) { let root, value, action; @@ -267,6 +278,29 @@ class ClapperPlayerBase extends GstPlayer.Player this.pipeline.flags = settingsFlags; debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`); break; + case 'webserver-enabled': + const webserverEnabled = settings.get_boolean(key); + + if(webserverEnabled) { + if(!WebServer) { + /* Probably most users will not use this, + * so conditional import for faster startup */ + WebServer = imports.clapper_src.webserver.WebServer; + } + + if(!this.webserver) + this.webserver = new WebServer(settings.get_int('webserver-port')); + + this.webserver.startListening(); + this.websocketSignal = this.webserver.connect( + 'websocket-data', this._onWsData.bind(this) + ); + } + else if(this.webserver) { + this.webserver.disconnect(this.websocketSignal); + this.webserver.stopListening(); + } + break; default: break; } diff --git a/data/com.github.rafostar.Clapper.gschema.xml b/data/com.github.rafostar.Clapper.gschema.xml index c18218b1..15f85b95 100644 --- a/data/com.github.rafostar.Clapper.gschema.xml +++ b/data/com.github.rafostar.Clapper.gschema.xml @@ -49,6 +49,16 @@ The subtitles font description + + + false + Enable WebSocket server for remote playback control + + + 6446 + Listening port to use for incoming WebSocket connections + + true From 104db83a1cff704bebb5425ff4318047adcbb3ee Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 12 Dec 2020 00:13:02 +0100 Subject: [PATCH 07/26] Clean websocket signal properly --- clapper_src/playerBase.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index ceabf855..f4aa4633 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -292,12 +292,16 @@ class ClapperPlayerBase extends GstPlayer.Player this.webserver = new WebServer(settings.get_int('webserver-port')); this.webserver.startListening(); - this.websocketSignal = this.webserver.connect( - 'websocket-data', this._onWsData.bind(this) - ); + + if(!this.websocketSignal) { + this.websocketSignal = this.webserver.connect( + 'websocket-data', this._onWsData.bind(this) + ); + } } else if(this.webserver) { this.webserver.disconnect(this.websocketSignal); + this.websocketSignal = null; this.webserver.stopListening(); } break; From 7a039af7986b121e2dd699b127fe6562ee03dd86 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 12 Dec 2020 00:16:39 +0100 Subject: [PATCH 08/26] Allow changing web server port during playback --- clapper_src/playerBase.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index f4aa4633..17f1ddd8 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -305,6 +305,12 @@ class ClapperPlayerBase extends GstPlayer.Player this.webserver.stopListening(); } break; + case 'webserver-port': + if(!this.webserver) + break; + + this.webserver.setListeningPort(settings.get_int(key)); + break; default: break; } From ea7b712b2e05a31effaea5e4600773b62513af0c Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 12 Dec 2020 19:37:07 +0100 Subject: [PATCH 09/26] Send player state via WebSockets --- clapper_src/player.js | 3 +++ clapper_src/playerBase.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/clapper_src/player.js b/clapper_src/player.js index be06fddc..fc396493 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -334,6 +334,7 @@ class ClapperPlayer extends PlayerBase _onStateChanged(player, state) { this.state = state; + this.emitWs('state-changed', state); if(state !== GstPlayer.PlayerState.BUFFERING) { let root = player.widget.get_root(); @@ -362,6 +363,8 @@ class ClapperPlayer extends PlayerBase _onStreamEnded(player) { debug('stream ended'); + this.emitWs('stream-ended', this._trackId); + this._trackId++; if(this._trackId < this._playlist.length) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index 17f1ddd8..3e24179a 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -181,6 +181,14 @@ class ClapperPlayerBase extends GstPlayer.Player this.widget.queue_render(); } + emitWs(action, value) + { + if(!this.webserver) + return; + + this.webserver.sendMessage({ action, value }); + } + _onWsData(server, action, value) { debug(`unhandled WebSocket action: ${action}`); From 660b5c6c48e5717968d4d28f493ed274ac3a1ce8 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 12 Dec 2020 20:10:06 +0100 Subject: [PATCH 10/26] Use underscore in WebSocket API --- clapper_src/player.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/clapper_src/player.js b/clapper_src/player.js index fc396493..e5884192 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -334,7 +334,7 @@ class ClapperPlayer extends PlayerBase _onStateChanged(player, state) { this.state = state; - this.emitWs('state-changed', state); + this.emitWs('state_changed', state); if(state !== GstPlayer.PlayerState.BUFFERING) { let root = player.widget.get_root(); @@ -362,8 +362,8 @@ class ClapperPlayer extends PlayerBase _onStreamEnded(player) { - debug('stream ended'); - this.emitWs('stream-ended', this._trackId); + debug(`end of stream: ${this._trackId}`); + this.emitWs('end_of_stream', this._trackId); this._trackId++; @@ -645,12 +645,11 @@ class ClapperPlayer extends PlayerBase _onWsData(server, action, value) { switch(action) { - case 'toggle-play': - this.toggle_play(); - break; + case 'toggle_play': case 'play': case 'pause': - this[action](); + case 'set_media': + this[action](value); break; default: super._onWsData(server, action, value); From 018a750fbc0ef32474c7236c04d68f6960570f3b Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sat, 12 Dec 2020 21:56:35 +0100 Subject: [PATCH 11/26] Add web app for broadway backend --- clapper_src/webapp.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 clapper_src/webapp.js diff --git a/clapper_src/webapp.js b/clapper_src/webapp.js new file mode 100644 index 00000000..3c43933b --- /dev/null +++ b/clapper_src/webapp.js @@ -0,0 +1,36 @@ +const { Gio, GObject } = imports.gi; +const Debug = imports.clapper_src.debug; +const Misc = imports.clapper_src.misc; + +let { debug } = Debug; + +var WebApp = GObject.registerClass( +class ClapperWebApp extends Gio.SubprocessLauncher +{ + _init() + { + const flags = Gio.SubprocessFlags.STDOUT_SILENCE + | Gio.SubprocessFlags.STDERR_SILENCE; + + super._init(flags); + } + + startRemoteApp() + { + this.setenv('GDK_BACKEND', 'broadway', true); + this.setenv('BROADWAY_DISPLAY', '6', true); + + this.remoteApp = this.spawnv(Misc.appId); + this.remoteApp.wait_async(null, this._onRemoteClosed.bind(this)); + + debug('remote app started'); + } + + _onRemoteClosed(remoteApp, res) + { + debug('remote app closed'); + + this.setenv('GDK_BACKEND', '', true); + this.setenv('BROADWAY_DISPLAY', '', true); + } +}); From ea67e1e62014d2f753e0e1aee4feaa62a178a4fc Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Mon, 14 Dec 2020 11:19:12 +0100 Subject: [PATCH 12/26] Flatpak: compile GTK4 with broadway backend --- pkgs/flatpak/lib/gtk4.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/flatpak/lib/gtk4.json b/pkgs/flatpak/lib/gtk4.json index 5f2a38eb..4a8da02a 100644 --- a/pkgs/flatpak/lib/gtk4.json +++ b/pkgs/flatpak/lib/gtk4.json @@ -3,6 +3,7 @@ "buildsystem": "meson", "config-opts": [ "--wrap-mode=nofallback", + "-Dbroadway-backend=true", "-Dwin32-backend=false", "-Dmacos-backend=false", "-Dmedia-ffmpeg=disabled", @@ -10,7 +11,8 @@ "-Dprint-cloudprint=disabled", "-Dintrospection=enabled", "-Ddemos=false", - "-Dbuild-examples=false" + "-Dbuild-examples=false", + "-Dbuild-tests=false" ], "sources": [ { From 062a30761386652b6d7f2d4f887d28bf8b14dd6e Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 11:51:25 +0100 Subject: [PATCH 13/26] Add stop method for web app --- clapper_src/webapp.js | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/clapper_src/webapp.js b/clapper_src/webapp.js index 3c43933b..f5dca76f 100644 --- a/clapper_src/webapp.js +++ b/clapper_src/webapp.js @@ -12,25 +12,52 @@ class ClapperWebApp extends Gio.SubprocessLauncher const flags = Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE; - super._init(flags); + super._init({ flags }); + + this.remoteApp = null; + this.isRemoteClosing = false; + + this.setenv('GDK_BACKEND', 'broadway', true); } startRemoteApp() { - this.setenv('GDK_BACKEND', 'broadway', true); - this.setenv('BROADWAY_DISPLAY', '6', true); + if(this.remoteApp) + return; - this.remoteApp = this.spawnv(Misc.appId); + this.remoteApp = this.spawnv([Misc.appId + 'Remote']); this.remoteApp.wait_async(null, this._onRemoteClosed.bind(this)); debug('remote app started'); } - _onRemoteClosed(remoteApp, res) + stopRemoteApp() { - debug('remote app closed'); + if(!this.remoteApp || this.isRemoteClosing) + return; - this.setenv('GDK_BACKEND', '', true); - this.setenv('BROADWAY_DISPLAY', '', true); + this.isRemoteClosing = true; + this.remoteApp.force_exit(); + + debug('send stop signal to remote app'); + } + + _onRemoteClosed(proc, result) + { + let hadError; + + try { + hadError = proc.wait_finish(result); + } + catch(err) { + debug(err); + } + + this.remoteApp = null; + + if(hadError) + debug('remote app exited with error'); + + debug('remote app closed'); } }); From b756c15e46f3d5ef7e0f9d7d9eb7fdb7417739d5 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 11:59:05 +0100 Subject: [PATCH 14/26] Fix non-updated closing state --- clapper_src/webapp.js | 1 + 1 file changed, 1 insertion(+) diff --git a/clapper_src/webapp.js b/clapper_src/webapp.js index f5dca76f..e5bd1168 100644 --- a/clapper_src/webapp.js +++ b/clapper_src/webapp.js @@ -54,6 +54,7 @@ class ClapperWebApp extends Gio.SubprocessLauncher } this.remoteApp = null; + this.isRemoteClosing = false; if(hadError) debug('remote app exited with error'); From b4e52d654b6450a77ad74e541254d20daf990724 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 12:36:06 +0100 Subject: [PATCH 15/26] Pass WebSocket data without additional signal connection --- clapper_src/player.js | 30 +++++++++++++++--------------- clapper_src/playerBase.js | 12 ++---------- clapper_src/webserver.js | 19 +++++++------------ 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/clapper_src/player.js b/clapper_src/player.js index e5884192..dc0091d6 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -251,6 +251,21 @@ class ClapperPlayer extends PlayerBase this[action](); } + receiveWs(action, value) + { + switch(action) { + case 'toggle_play': + case 'play': + case 'pause': + case 'set_media': + this[action](value); + break; + default: + super.receiveWs(action, value); + break; + } + } + _setHideCursorTimeout() { this._clearTimeout('hideCursor'); @@ -642,21 +657,6 @@ class ClapperPlayer extends PlayerBase return true; } - _onWsData(server, action, value) - { - switch(action) { - case 'toggle_play': - case 'play': - case 'pause': - case 'set_media': - this[action](value); - break; - default: - super._onWsData(server, action, value); - break; - } - } - _onCloseRequest(window) { this._performCloseCleanup(window); diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index 3e24179a..7708539c 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -69,7 +69,6 @@ class ClapperPlayerBase extends GstPlayer.Player this.visualization_enabled = false; this.webserver = null; - this.websocketSignal = null; this.set_all_plugins_ranks(); this.set_initial_config(); @@ -189,7 +188,7 @@ class ClapperPlayerBase extends GstPlayer.Player this.webserver.sendMessage({ action, value }); } - _onWsData(server, action, value) + receiveWs(action, value) { debug(`unhandled WebSocket action: ${action}`); } @@ -300,16 +299,9 @@ class ClapperPlayerBase extends GstPlayer.Player this.webserver = new WebServer(settings.get_int('webserver-port')); this.webserver.startListening(); - - if(!this.websocketSignal) { - this.websocketSignal = this.webserver.connect( - 'websocket-data', this._onWsData.bind(this) - ); - } + this.webserver.passMsgData = this.receiveWs.bind(this); } else if(this.webserver) { - this.webserver.disconnect(this.websocketSignal); - this.websocketSignal = null; this.webserver.stopListening(); } break; diff --git a/clapper_src/webserver.js b/clapper_src/webserver.js index 2548003a..626a110e 100644 --- a/clapper_src/webserver.js +++ b/clapper_src/webserver.js @@ -4,17 +4,8 @@ const Debug = imports.clapper_src.debug; let { debug } = Debug; -var WebServer = GObject.registerClass({ - Signals: { - 'websocket-data': { - flags: GObject.SignalFlags.RUN_FIRST, - param_types: [ - GObject.TYPE_STRING, - GObject.TYPE_INT || GObject.TYPE_STRING - ] - } - } -}, class ClapperWebServer extends Soup.Server +var WebServer = GObject.registerClass( +class ClapperWebServer extends Soup.Server { _init(port) { @@ -96,6 +87,10 @@ var WebServer = GObject.registerClass({ } } + passMsgData(action, value) + { + } + _closeCleanup() { while(this.wsConns.length) { @@ -140,7 +135,7 @@ var WebServer = GObject.registerClass({ if(!parsedMsg || !parsedMsg.action) return debug('no "action" in parsed WebSocket message'); - this.emit('websocket-data', parsedMsg.action, parsedMsg.value || 0); + this.passMsgData(parsedMsg.action, parsedMsg.value || 0); } _onWsClosed(connection) From ca6322339f6e336de0071511d78c20079c3a00ed Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:05:48 +0100 Subject: [PATCH 16/26] Do not forget port after web server is stopped --- clapper_src/webserver.js | 1 - 1 file changed, 1 deletion(-) diff --git a/clapper_src/webserver.js b/clapper_src/webserver.js index 626a110e..0824f344 100644 --- a/clapper_src/webserver.js +++ b/clapper_src/webserver.js @@ -74,7 +74,6 @@ class ClapperWebServer extends Soup.Server this.disconnect(); this.isListening = false; - this.listeningPort = null; } sendMessage(data) From 6d4cd494fe68b54f6f63244e9d45bfbd9813e5a3 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:16:31 +0100 Subject: [PATCH 17/26] Customize web server listening port --- clapper_src/prefs.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/clapper_src/prefs.js b/clapper_src/prefs.js index 43e4467b..634647e5 100644 --- a/clapper_src/prefs.js +++ b/clapper_src/prefs.js @@ -95,8 +95,16 @@ class ClapperNetworkPage extends PrefsBase.Grid { super._init(); + let checkButton; + let spinButton; + this.addTitle('Client'); this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD); + + this.addTitle('Server'); + checkButton = this.addCheckButton('Allow remote control of the player', 'webserver-enabled'); + spinButton = this.addSpinButton('Listening port', 1024, 65535, 'webserver-port'); + checkButton.bind_property('active', spinButton, 'visible', GObject.BindingFlags.SYNC_CREATE); } }); From 7431f5803464c95d64d5f2ccccac7b3527e51ec9 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 18:15:40 +0100 Subject: [PATCH 18/26] Prefer "set_playlist" over "set_media" method --- clapper_src/dialogs.js | 2 +- clapper_src/player.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clapper_src/dialogs.js b/clapper_src/dialogs.js index 94d698b7..8e36ab9e 100644 --- a/clapper_src/dialogs.js +++ b/clapper_src/dialogs.js @@ -133,7 +133,7 @@ class ClapperUriDialog extends Gtk.Dialog openUri(uri) { let { player } = this.get_transient_for().get_child(); - player.set_media(uri); + player.set_playlist([uri]); this.close(); } diff --git a/clapper_src/player.js b/clapper_src/player.js index dc0091d6..48f09e29 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -257,7 +257,7 @@ class ClapperPlayer extends PlayerBase case 'toggle_play': case 'play': case 'pause': - case 'set_media': + case 'set_playlist': this[action](value); break; default: From 4c6e5607fb6ded472f5b3513145dbaa75c184c2f Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 18:16:59 +0100 Subject: [PATCH 19/26] Check if player has widget before trying to focus it --- clapper_src/headerbarBase.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/clapper_src/headerbarBase.js b/clapper_src/headerbarBase.js index 09d32b40..6eed791c 100644 --- a/clapper_src/headerbarBase.js +++ b/clapper_src/headerbarBase.js @@ -123,7 +123,13 @@ class ClapperHeaderBarPopover extends Gtk.PopoverMenu let root = this.get_root(); let clapperWidget = root.get_child(); - if(clapperWidget && clapperWidget.player) - clapperWidget.player.widget.grab_focus(); + if( + !clapperWidget + || !clapperWidget.player + || !clapperWidget.player.widget + ) + return; + + clapperWidget.player.widget.grab_focus(); } }); From 8564cc96179c7915b02bc578cdd6a3c2a11e8eb5 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 18:19:24 +0100 Subject: [PATCH 20/26] Move WebSocket message parsing to another file Allows reusing the same code for the client app --- clapper_src/webHelpers.js | 30 ++++++++++++++++++++++++++++++ clapper_src/webserver.js | 21 ++++----------------- 2 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 clapper_src/webHelpers.js diff --git a/clapper_src/webHelpers.js b/clapper_src/webHelpers.js new file mode 100644 index 00000000..571e6726 --- /dev/null +++ b/clapper_src/webHelpers.js @@ -0,0 +1,30 @@ +const { Soup } = imports.gi; +const ByteArray = imports.byteArray; +const Debug = imports.clapper_src.debug; + +let { debug } = Debug; + +function parseData(dataType, bytes) +{ + if(dataType !== Soup.WebsocketDataType.TEXT) { + debug('ignoring non-text WebSocket message'); + return [false]; + } + + let parsedMsg = null; + const msg = bytes.get_data(); + + try { + parsedMsg = JSON.parse(ByteArray.toString(msg)); + } + catch(err) { + debug(err); + } + + if(!parsedMsg || !parsedMsg.action) { + debug('no "action" in parsed WebSocket message'); + return [false]; + } + + return [true, parsedMsg]; +} diff --git a/clapper_src/webserver.js b/clapper_src/webserver.js index 0824f344..72895564 100644 --- a/clapper_src/webserver.js +++ b/clapper_src/webserver.js @@ -1,6 +1,6 @@ const { Soup, GObject } = imports.gi; -const ByteArray = imports.byteArray; const Debug = imports.clapper_src.debug; +const WebHelpers = imports.clapper_src.webHelpers; let { debug } = Debug; @@ -118,23 +118,10 @@ class ClapperWebServer extends Soup.Server _onWsMessage(connection, dataType, bytes) { - if(dataType !== Soup.WebsocketDataType.TEXT) - return debug('ignoring non-text WebSocket message'); + const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes); - const msg = bytes.get_data(); - let parsedMsg = null; - - try { - parsedMsg = JSON.parse(ByteArray.toString(msg)); - } - catch(err) { - debug(err); - } - - if(!parsedMsg || !parsedMsg.action) - return debug('no "action" in parsed WebSocket message'); - - this.passMsgData(parsedMsg.action, parsedMsg.value || 0); + if(success) + this.passMsgData(parsedMsg.action, parsedMsg.value); } _onWsClosed(connection) From 5231a1f2251003e73aaf08537f758a718c32deab Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 18:20:48 +0100 Subject: [PATCH 21/26] Add initial WebSocket client app --- clapper_src/appRemote.js | 4 ++ clapper_src/playerRemote.js | 24 +++++++++++ clapper_src/webClient.js | 82 +++++++++++++++++++++++++++++++++++++ clapper_src/widgetRemote.js | 13 ++++++ 4 files changed, 123 insertions(+) create mode 100644 clapper_src/playerRemote.js create mode 100644 clapper_src/webClient.js create mode 100644 clapper_src/widgetRemote.js diff --git a/clapper_src/appRemote.js b/clapper_src/appRemote.js index ae34f9c2..2bbc8877 100644 --- a/clapper_src/appRemote.js +++ b/clapper_src/appRemote.js @@ -1,6 +1,7 @@ const { GObject } = imports.gi; const { AppBase } = imports.clapper_src.appBase; const { HeaderBarBase } = imports.clapper_src.headerbarBase; +const { WidgetRemote } = imports.clapper_src.widgetRemote; var AppRemote = GObject.registerClass( class ClapperAppRemote extends AppBase @@ -9,6 +10,9 @@ class ClapperAppRemote extends AppBase { super.vfunc_startup(); + let clapperWidget = new WidgetRemote(); + this.active_window.set_child(clapperWidget); + let headerBar = new HeaderBarBase(this.active_window); this.active_window.set_titlebar(headerBar); diff --git a/clapper_src/playerRemote.js b/clapper_src/playerRemote.js new file mode 100644 index 00000000..6a5b4c4d --- /dev/null +++ b/clapper_src/playerRemote.js @@ -0,0 +1,24 @@ +const { GObject } = imports.gi; +const Misc = imports.clapper_src.misc; +const { WebClient } = imports.clapper_src.webClient; + +let { settings } = Misc; + +var PlayerRemote = GObject.registerClass( +class ClapperPlayerRemote extends GObject.Object +{ + _init() + { + super._init(); + + this.webclient = new WebClient(settings.get_int('webserver-port')); + } + + set_playlist(playlist) + { + this.webclient.sendMessage({ + action: 'set_playlist', + value: playlist + }); + } +}); diff --git a/clapper_src/webClient.js b/clapper_src/webClient.js new file mode 100644 index 00000000..a1397f45 --- /dev/null +++ b/clapper_src/webClient.js @@ -0,0 +1,82 @@ +const { Soup, GObject } = imports.gi; +const Debug = imports.clapper_src.debug; +const WebHelpers = imports.clapper_src.webHelpers; + +let { debug } = Debug; + +var WebClient = GObject.registerClass( +class ClapperWebClient extends Soup.Session +{ + _init(port) + { + super._init({ + timeout: 3, + use_thread_context: true, + }); + + this.wsConn = null; + + this.connectWebsocket(port); + } + + connectWebsocket(port) + { + if(this.wsConn) + return; + + let message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`); + this.websocket_connect_async(message, null, null, null, this._onWsConnect.bind(this)); + + debug('connecting WebSocket to Clapper app'); + } + + sendMessage(data) + { + if( + !this.wsConn + || this.wsConn.state !== Soup.WebsocketState.OPEN + ) + return; + + this.wsConn.send_text(JSON.stringify(data)); + } + + passMsgData(action, value) + { + } + + _onWsConnect(session, result) + { + let connection = null; + + try { + connection = this.websocket_connect_finish(result); + } + catch(err) { + debug(err); + } + + if(!connection) + return; + + connection.connect('message', this._onWsMessage.bind(this)); + connection.connect('closed', this._onWsClosed.bind(this)); + + this.wsConn = connection; + + debug('successfully connected WebSocket'); + } + + _onWsMessage(connection, dataType, bytes) + { + const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes); + + if(success) + this.passMsgData(parsedMsg.action, parsedMsg.value); + } + + _onWsClosed(connection) + { + debug('closed WebSocket connection'); + } +}); diff --git a/clapper_src/widgetRemote.js b/clapper_src/widgetRemote.js new file mode 100644 index 00000000..79a95961 --- /dev/null +++ b/clapper_src/widgetRemote.js @@ -0,0 +1,13 @@ +const { GObject, Gtk } = imports.gi; +const { PlayerRemote } = imports.clapper_src.playerRemote; + +var WidgetRemote = GObject.registerClass( +class ClapperWidgetRemote extends Gtk.Grid +{ + _init(opts) + { + super._init(); + + this.player = new PlayerRemote(); + } +}); From dde35270ffb1da2a2932855701cb7e409dc26665 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 18:27:18 +0100 Subject: [PATCH 22/26] Consistent source filenames --- clapper_src/playerBase.js | 2 +- clapper_src/{webapp.js => webApp.js} | 0 clapper_src/{webserver.js => webServer.js} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename clapper_src/{webapp.js => webApp.js} (100%) rename clapper_src/{webserver.js => webServer.js} (100%) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index 7708539c..facb9ba8 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -292,7 +292,7 @@ class ClapperPlayerBase extends GstPlayer.Player if(!WebServer) { /* Probably most users will not use this, * so conditional import for faster startup */ - WebServer = imports.clapper_src.webserver.WebServer; + WebServer = imports.clapper_src.webServer.WebServer; } if(!this.webserver) diff --git a/clapper_src/webapp.js b/clapper_src/webApp.js similarity index 100% rename from clapper_src/webapp.js rename to clapper_src/webApp.js diff --git a/clapper_src/webserver.js b/clapper_src/webServer.js similarity index 100% rename from clapper_src/webserver.js rename to clapper_src/webServer.js From a1e95dc012616375659388c497bc7a8a0cc42b62 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 19:03:58 +0100 Subject: [PATCH 23/26] Close remote app on error or disconnect --- clapper_src/webClient.js | 4 +++- clapper_src/widgetRemote.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/clapper_src/webClient.js b/clapper_src/webClient.js index a1397f45..938586dc 100644 --- a/clapper_src/webClient.js +++ b/clapper_src/webClient.js @@ -57,7 +57,7 @@ class ClapperWebClient extends Soup.Session } if(!connection) - return; + return this.passMsgData('close'); connection.connect('message', this._onWsMessage.bind(this)); connection.connect('closed', this._onWsClosed.bind(this)); @@ -78,5 +78,7 @@ class ClapperWebClient extends Soup.Session _onWsClosed(connection) { debug('closed WebSocket connection'); + + this.passMsgData('close'); } }); diff --git a/clapper_src/widgetRemote.js b/clapper_src/widgetRemote.js index 79a95961..ba0cddfe 100644 --- a/clapper_src/widgetRemote.js +++ b/clapper_src/widgetRemote.js @@ -9,5 +9,18 @@ class ClapperWidgetRemote extends Gtk.Grid super._init(); this.player = new PlayerRemote(); + this.player.webclient.passMsgData = this.receiveWs.bind(this); + } + + receiveWs(action, value) + { + switch(action) { + case 'close': + let root = this.get_root(); + root.run_dispose(); + break; + default: + break; + } } }); From a056fac1c1f7851d9629e3ca794ef540daa529bf Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 22:35:14 +0100 Subject: [PATCH 24/26] Add logic responsible for starting web app --- clapper_src/playerBase.js | 28 ++++++++++++++++---- clapper_src/playerRemote.js | 5 +--- clapper_src/prefs.js | 11 ++++---- clapper_src/webClient.js | 12 ++++++--- clapper_src/webServer.js | 2 +- data/com.github.rafostar.Clapper.gschema.xml | 4 +++ 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index facb9ba8..1dc851fc 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -1,6 +1,7 @@ const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; +const { WebApp } = imports.clapper_src.webApp; let WebServer; @@ -69,6 +70,7 @@ class ClapperPlayerBase extends GstPlayer.Player this.visualization_enabled = false; this.webserver = null; + this.webapp = null; this.set_all_plugins_ranks(); this.set_initial_config(); @@ -93,7 +95,7 @@ class ClapperPlayerBase extends GstPlayer.Player 'audio-offset', 'subtitle-offset', 'play-flags', - 'webserver-enabled', + 'webserver-enabled' ]; for(let key of settingsToSet) @@ -286,7 +288,8 @@ class ClapperPlayerBase extends GstPlayer.Player debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`); break; case 'webserver-enabled': - const webserverEnabled = settings.get_boolean(key); + case 'webapp-enabled': + const webserverEnabled = settings.get_boolean('webserver-enabled'); if(webserverEnabled) { if(!WebServer) { @@ -295,13 +298,28 @@ class ClapperPlayerBase extends GstPlayer.Player WebServer = imports.clapper_src.webServer.WebServer; } - if(!this.webserver) + if(!this.webserver) { this.webserver = new WebServer(settings.get_int('webserver-port')); - + this.webserver.passMsgData = this.receiveWs.bind(this); + } this.webserver.startListening(); - this.webserver.passMsgData = this.receiveWs.bind(this); + + const webappEnabled = settings.get_boolean('webapp-enabled'); + + if(!this.webapp && !webappEnabled) + break; + + if(webappEnabled) { + if(!this.webapp) + this.webapp = new WebApp(); + + this.webapp.startRemoteApp(); + } + else + this.webapp.stopRemoteApp(); } else if(this.webserver) { + /* remote app will close too when connection is lost */ this.webserver.stopListening(); } break; diff --git a/clapper_src/playerRemote.js b/clapper_src/playerRemote.js index 6a5b4c4d..166dd18c 100644 --- a/clapper_src/playerRemote.js +++ b/clapper_src/playerRemote.js @@ -1,9 +1,6 @@ const { GObject } = imports.gi; -const Misc = imports.clapper_src.misc; const { WebClient } = imports.clapper_src.webClient; -let { settings } = Misc; - var PlayerRemote = GObject.registerClass( class ClapperPlayerRemote extends GObject.Object { @@ -11,7 +8,7 @@ class ClapperPlayerRemote extends GObject.Object { super._init(); - this.webclient = new WebClient(settings.get_int('webserver-port')); + this.webclient = new WebClient(); } set_playlist(playlist) diff --git a/clapper_src/prefs.js b/clapper_src/prefs.js index 634647e5..57bf7cd5 100644 --- a/clapper_src/prefs.js +++ b/clapper_src/prefs.js @@ -95,16 +95,15 @@ class ClapperNetworkPage extends PrefsBase.Grid { super._init(); - let checkButton; - let spinButton; - this.addTitle('Client'); this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD); this.addTitle('Server'); - checkButton = this.addCheckButton('Allow remote control of the player', 'webserver-enabled'); - spinButton = this.addSpinButton('Listening port', 1024, 65535, 'webserver-port'); - checkButton.bind_property('active', spinButton, 'visible', GObject.BindingFlags.SYNC_CREATE); + let webServer = this.addCheckButton('Control player remotely', 'webserver-enabled'); + let serverPort = this.addSpinButton('Listening port', 1024, 65535, 'webserver-port'); + webServer.bind_property('active', serverPort, 'visible', GObject.BindingFlags.SYNC_CREATE); + //let webApp = this.addCheckButton('Run built-in web application', 'webapp-enabled'); + //webServer.bind_property('active', webApp, 'visible', GObject.BindingFlags.SYNC_CREATE); } }); diff --git a/clapper_src/webClient.js b/clapper_src/webClient.js index 938586dc..168db203 100644 --- a/clapper_src/webClient.js +++ b/clapper_src/webClient.js @@ -1,8 +1,10 @@ -const { Soup, GObject } = imports.gi; +const { Gio, GObject, Soup } = imports.gi; const Debug = imports.clapper_src.debug; +const Misc = imports.clapper_src.misc; const WebHelpers = imports.clapper_src.webHelpers; let { debug } = Debug; +let { settings } = Misc; var WebClient = GObject.registerClass( class ClapperWebClient extends Soup.Session @@ -16,15 +18,16 @@ class ClapperWebClient extends Soup.Session this.wsConn = null; - this.connectWebsocket(port); + this.connectWebsocket(); } - connectWebsocket(port) + connectWebsocket() { if(this.wsConn) return; - let message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`); + const port = settings.get_int('webserver-port'); + const message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`); this.websocket_connect_async(message, null, null, null, this._onWsConnect.bind(this)); debug('connecting WebSocket to Clapper app'); @@ -78,6 +81,7 @@ class ClapperWebClient extends Soup.Session _onWsClosed(connection) { debug('closed WebSocket connection'); + this.wsConn = null; this.passMsgData('close'); } diff --git a/clapper_src/webServer.js b/clapper_src/webServer.js index 72895564..8e30f408 100644 --- a/clapper_src/webServer.js +++ b/clapper_src/webServer.js @@ -55,7 +55,7 @@ class ClapperWebServer extends Soup.Server if(isListening) { const uris = this.get_uris(); const usedPort = uris[0].get_port(); - debug(`WebSocket server listening on port: ${usedPort}`); + debug(`WebSocket server started listening on port: ${usedPort}`); } else { debug(new Error('WebSocket server could not start listening')); diff --git a/data/com.github.rafostar.Clapper.gschema.xml b/data/com.github.rafostar.Clapper.gschema.xml index 15f85b95..24a346db 100644 --- a/data/com.github.rafostar.Clapper.gschema.xml +++ b/data/com.github.rafostar.Clapper.gschema.xml @@ -58,6 +58,10 @@ 6446 Listening port to use for incoming WebSocket connections + + false + Run built-in broadway based web application + From 234451f62a51927b9b69bcc88a9172d4bd82c67d Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 22:49:06 +0100 Subject: [PATCH 25/26] Move defined play flags to prefs They are used only in prefs and it allows starting prefs in web app. --- clapper_src/playerBase.js | 21 ++------------------- clapper_src/prefs.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index 1dc851fc..a02ed2e1 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -3,28 +3,11 @@ const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; const { WebApp } = imports.clapper_src.webApp; -let WebServer; - -/* PlayFlags are not exported through GI */ -Gst.PlayFlags = { - VIDEO: 1, - AUDIO: 2, - TEXT: 4, - VIS: 8, - SOFT_VOLUME: 16, - NATIVE_AUDIO: 32, - NATIVE_VIDEO: 64, - DOWNLOAD: 128, - BUFFERING: 256, - DEINTERLACE: 512, - SOFT_COLORBALANCE: 1024, - FORCE_FILTERS: 2048, - FORCE_SW_DECODERS: 4096, -}; - let { debug } = Debug; let { settings } = Misc; +let WebServer; + var PlayerBase = GObject.registerClass( class ClapperPlayerBase extends GstPlayer.Player { diff --git a/clapper_src/prefs.js b/clapper_src/prefs.js index 57bf7cd5..5f1e09ec 100644 --- a/clapper_src/prefs.js +++ b/clapper_src/prefs.js @@ -4,6 +4,23 @@ const PrefsBase = imports.clapper_src.prefsBase; let { settings } = Misc; +/* PlayFlags are not exported through GI */ +Gst.PlayFlags = { + VIDEO: 1, + AUDIO: 2, + TEXT: 4, + VIS: 8, + SOFT_VOLUME: 16, + NATIVE_AUDIO: 32, + NATIVE_VIDEO: 64, + DOWNLOAD: 128, + BUFFERING: 256, + DEINTERLACE: 512, + SOFT_COLORBALANCE: 1024, + FORCE_FILTERS: 2048, + FORCE_SW_DECODERS: 4096, +}; + var GeneralPage = GObject.registerClass( class ClapperGeneralPage extends PrefsBase.Grid { From b6c947efa6a59fd49c29b8712141c4f86aa18f07 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 15 Dec 2020 23:26:24 +0100 Subject: [PATCH 26/26] Fix custom CSS loading for remote app --- clapper_src/misc.js | 15 ++++++++++++++- clapper_src/widget.js | 18 ++++-------------- clapper_src/widgetRemote.js | 3 +++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/clapper_src/misc.js b/clapper_src/misc.js index b1ef3e2f..46b234f4 100644 --- a/clapper_src/misc.js +++ b/clapper_src/misc.js @@ -1,4 +1,4 @@ -const { Gio, GstAudio, GstPlayer, Gtk } = imports.gi; +const { Gio, GstAudio, GstPlayer, Gdk, Gtk } = imports.gi; const Debug = imports.clapper_src.debug; var appName = 'Clapper'; @@ -34,6 +34,19 @@ function getClapperVersion() : ''; } +function loadCustomCss() +{ + const clapperPath = getClapperPath(); + const cssProvider = new Gtk.CssProvider(); + + cssProvider.load_from_path(`${clapperPath}/css/styles.css`); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + cssProvider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); +} + function inhibitForState(state, window) { let isInhibited = false; diff --git a/clapper_src/widget.js b/clapper_src/widget.js index 707174a1..c1acab14 100644 --- a/clapper_src/widget.js +++ b/clapper_src/widget.js @@ -16,25 +16,15 @@ var Widget = GObject.registerClass({ } }, class ClapperWidget extends Gtk.Grid { - _init(opts) + _init() { Debug.gstVersionCheck(); super._init(); - let clapperPath = Misc.getClapperPath(); - let defaults = { - cssPath: `${clapperPath}/css/styles.css`, - }; - Object.assign(this, defaults, opts); - - let cssProvider = new Gtk.CssProvider(); - cssProvider.load_from_path(this.cssPath); - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - cssProvider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); + /* load CSS here to allow using this class + * separately as a pre-made GTK widget */ + Misc.loadCustomCss(); this.windowSize = JSON.parse(settings.get_string('window-size')); this.floatSize = JSON.parse(settings.get_string('float-size')); diff --git a/clapper_src/widgetRemote.js b/clapper_src/widgetRemote.js index ba0cddfe..9cf6c107 100644 --- a/clapper_src/widgetRemote.js +++ b/clapper_src/widgetRemote.js @@ -1,4 +1,5 @@ const { GObject, Gtk } = imports.gi; +const Misc = imports.clapper_src.misc; const { PlayerRemote } = imports.clapper_src.playerRemote; var WidgetRemote = GObject.registerClass( @@ -8,6 +9,8 @@ class ClapperWidgetRemote extends Gtk.Grid { super._init(); + Misc.loadCustomCss(); + this.player = new PlayerRemote(); this.player.webclient.passMsgData = this.receiveWs.bind(this); }