Merge pull request #29 from Rafostar/remote-controller

Control player remotely
This commit is contained in:
Rafostar
2020-12-16 10:55:32 +01:00
committed by GitHub
25 changed files with 838 additions and 256 deletions

View File

@@ -9,4 +9,4 @@ Package.init({
libdir: '@libdir@',
datadir: '@datadir@',
});
Package.run(imports.main);
Package.run(imports.clapper_src.main);

View File

@@ -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);

View File

@@ -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

View File

@@ -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}`);
}
});

112
clapper_src/appBase.js Normal file
View File

@@ -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}`);
}
});

21
clapper_src/appRemote.js Normal file
View File

@@ -0,0 +1,21 @@
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
{
vfunc_startup()
{
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);
this.active_window.maximize();
}
});

View File

@@ -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();
}

View File

@@ -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();
}
});

View File

@@ -0,0 +1,135 @@
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
)
return;
clapperWidget.player.widget.grab_focus();
}
});

17
clapper_src/mainRemote.js Normal file
View File

@@ -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();
}

View File

@@ -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';
@@ -35,6 +35,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;

View File

@@ -251,6 +251,21 @@ class ClapperPlayer extends PlayerBase
this[action]();
}
receiveWs(action, value)
{
switch(action) {
case 'toggle_play':
case 'play':
case 'pause':
case 'set_playlist':
this[action](value);
break;
default:
super.receiveWs(action, value);
break;
}
}
_setHideCursorTimeout()
{
this._clearTimeout('hideCursor');
@@ -337,6 +352,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();
@@ -364,7 +380,9 @@ class ClapperPlayer extends PlayerBase
_onStreamEnded(player)
{
debug('stream ended');
debug(`end of stream: ${this._trackId}`);
this.emitWs('end_of_stream', this._trackId);
this._trackId++;
if(this._trackId < this._playlist.length)

View File

@@ -1,27 +1,13 @@
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
const Debug = imports.clapper_src.debug;
const Misc = imports.clapper_src.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,
};
const { WebApp } = imports.clapper_src.webApp;
let { debug } = Debug;
let { settings } = Misc;
let WebServer;
var PlayerBase = GObject.registerClass(
class ClapperPlayerBase extends GstPlayer.Player
{
@@ -66,6 +52,9 @@ class ClapperPlayerBase extends GstPlayer.Player
this.state = GstPlayer.PlayerState.STOPPED;
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.set_all_plugins_ranks();
this.set_initial_config();
this.set_and_bind_settings();
@@ -89,6 +78,7 @@ class ClapperPlayerBase extends GstPlayer.Player
'audio-offset',
'subtitle-offset',
'play-flags',
'webserver-enabled'
];
for(let key of settingsToSet)
@@ -175,6 +165,19 @@ class ClapperPlayerBase extends GstPlayer.Player
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;
@@ -267,6 +270,48 @@ class ClapperPlayerBase extends GstPlayer.Player
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.clapper_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.startRemoteApp();
}
else
this.webapp.stopRemoteApp();
}
else if(this.webserver) {
/* remote app will close too when connection is lost */
this.webserver.stopListening();
}
break;
case 'webserver-port':
if(!this.webserver)
break;
this.webserver.setListeningPort(settings.get_int(key));
break;
default:
break;
}

View File

@@ -0,0 +1,21 @@
const { GObject } = imports.gi;
const { WebClient } = imports.clapper_src.webClient;
var PlayerRemote = GObject.registerClass(
class ClapperPlayerRemote extends GObject.Object
{
_init()
{
super._init();
this.webclient = new WebClient();
}
set_playlist(playlist)
{
this.webclient.sendMessage({
action: 'set_playlist',
value: playlist
});
}
});

View File

@@ -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
{
@@ -97,6 +114,13 @@ class ClapperNetworkPage extends PrefsBase.Grid
this.addTitle('Client');
this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD);
this.addTitle('Server');
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);
}
});

64
clapper_src/webApp.js Normal file
View File

@@ -0,0 +1,64 @@
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 });
this.remoteApp = null;
this.isRemoteClosing = false;
this.setenv('GDK_BACKEND', 'broadway', true);
}
startRemoteApp()
{
if(this.remoteApp)
return;
this.remoteApp = this.spawnv([Misc.appId + 'Remote']);
this.remoteApp.wait_async(null, this._onRemoteClosed.bind(this));
debug('remote app started');
}
stopRemoteApp()
{
if(!this.remoteApp || this.isRemoteClosing)
return;
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;
this.isRemoteClosing = false;
if(hadError)
debug('remote app exited with error');
debug('remote app closed');
}
});

88
clapper_src/webClient.js Normal file
View File

@@ -0,0 +1,88 @@
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
{
_init(port)
{
super._init({
timeout: 3,
use_thread_context: true,
});
this.wsConn = null;
this.connectWebsocket();
}
connectWebsocket()
{
if(this.wsConn)
return;
const port = settings.get_int('webserver-port');
const message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`);
this.websocket_connect_async(message, null, null, null, this._onWsConnect.bind(this));
debug('connecting WebSocket to Clapper app');
}
sendMessage(data)
{
if(
!this.wsConn
|| this.wsConn.state !== Soup.WebsocketState.OPEN
)
return;
this.wsConn.send_text(JSON.stringify(data));
}
passMsgData(action, value)
{
}
_onWsConnect(session, result)
{
let connection = null;
try {
connection = this.websocket_connect_finish(result);
}
catch(err) {
debug(err);
}
if(!connection)
return this.passMsgData('close');
connection.connect('message', this._onWsMessage.bind(this));
connection.connect('closed', this._onWsClosed.bind(this));
this.wsConn = connection;
debug('successfully connected WebSocket');
}
_onWsMessage(connection, dataType, bytes)
{
const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes);
if(success)
this.passMsgData(parsedMsg.action, parsedMsg.value);
}
_onWsClosed(connection)
{
debug('closed WebSocket connection');
this.wsConn = null;
this.passMsgData('close');
}
});

30
clapper_src/webHelpers.js Normal file
View File

@@ -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];
}

139
clapper_src/webServer.js Normal file
View File

@@ -0,0 +1,139 @@
const { Soup, GObject } = imports.gi;
const Debug = imports.clapper_src.debug;
const WebHelpers = imports.clapper_src.webHelpers;
let { debug } = Debug;
var WebServer = GObject.registerClass(
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 started 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;
}
sendMessage(data)
{
for(const connection of this.wsConns) {
if(connection.state !== Soup.WebsocketState.OPEN)
continue;
connection.send_text(JSON.stringify(data));
}
}
passMsgData(action, value)
{
}
_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)
{
const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes);
if(success)
this.passMsgData(parsedMsg.action, parsedMsg.value);
}
_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;
}
});

View File

@@ -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'));

View File

@@ -0,0 +1,29 @@
const { GObject, Gtk } = imports.gi;
const Misc = imports.clapper_src.misc;
const { PlayerRemote } = imports.clapper_src.playerRemote;
var WidgetRemote = GObject.registerClass(
class ClapperWidgetRemote extends Gtk.Grid
{
_init(opts)
{
super._init();
Misc.loadCustomCss();
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;
}
}
});

View File

@@ -49,6 +49,20 @@
<summary>The subtitles font description</summary>
</key>
<!-- Network -->
<key name="webserver-enabled" type="b">
<default>false</default>
<summary>Enable WebSocket server for remote playback control</summary>
</key>
<key name="webserver-port" type="i">
<default>6446</default>
<summary>Listening port to use for incoming WebSocket connections</summary>
</key>
<key name="webapp-enabled" type="b">
<default>false</default>
<summary>Run built-in broadway based web application</summary>
</key>
<!-- Tweaks -->
<key name="dark-theme" type="b">
<default>true</default>

View File

@@ -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')

View File

@@ -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": [
{