Merge pull request #8 from Rafostar/GTK4

We are moving to GTK4! Porting took a little longer than expected, cause I had to port GStreamer first. This change will be problematic for users of non-rolling linux distros, but its worth it. Both GTK4 and GStreamer 1.18+ have important GL changes. Clapper player takes full advantage of them.
This commit is contained in:
Rafostar
2020-10-14 18:52:23 +02:00
committed by GitHub
15 changed files with 720 additions and 613 deletions

132
README.md
View File

@@ -28,10 +28,11 @@ Using hardware acceleration is highly recommended. As stated in `GStreamer` wiki
In the case of OpenGL based elements, the buffers have the GstVideoGLTextureUploadMeta meta, which
efficiently copies the content of the VA-API surface into a GL texture.
```
Clapper uses `OpenGL` based sinks, so when `VA-API` is available, both CPU and RAM usage is much lower.
Clapper uses `OpenGL` based sinks, so when `VA-API` is available, both CPU and RAM usage is much lower. Especially if you have `gst-plugins-bad` 1.18+ with new `vah264dec` decoder which shares a single GL context with Clapper and uses DRM connection. If you have an AMD/Intel GPU, I highly recommend this new decoder.
To use `VA-API` make sure you have `gstreamer1-vaapi` installed. Verify with:
To use `VA-API` with H.264 videos, make sure you have `gst-plugins-bad` 1.18+. For other codecs additionally install `gstreamer1-vaapi`. Verify with:
```shell
gst-inspect-1.0 vah264dec
gst-inspect-1.0 vaapi
```
On some older GPUs you might need to export `GST_VAAPI_ALL_DRIVERS=1` environment variable.
@@ -40,11 +41,13 @@ Other acceleration methods (supported by `GStreamer`) should also work, but I ha
</details>
## Requirements
Clapper uses `GStreamer` bindings from `GI` repository, so if your distro ships them as separate package, they must be installed first.
Clapper uses GTK4 along with `GStreamer` bindings from `GI` repository, so if your distro ships them as separate package, they must be installed first.
Additionally Clapper requires these `GStreamer` elements:
* [gtkglsink](https://gstreamer.freedesktop.org/documentation/gtk/gtkglsink.html)
* [gtk4glsink](https://gstreamer.freedesktop.org/documentation/gtk/gtkglsink.html)
* [glsinkbin](https://gstreamer.freedesktop.org/documentation/opengl/glsinkbin.html)
**Attention:** `gtk4glsink` is my own port of current GStreamer `gtkglsink` to GTK4. The element is not part of GStreamer yet (pending review). Fedora package is available in my OBS repository. It will be installed along with Clapper if you add my repo to `dnf` package manager. Otherwise you might want to build it yourself from [source code](https://gitlab.freedesktop.org/Rafostar/gst-plugins-good/-/tree/GTK4) of my gstreamer GTK4 branch.
Other required plugins (codecs) depend on video format.
Recommended additional packages you should install manually via package manager:
@@ -53,69 +56,90 @@ Recommended additional packages you should install manually via package manager:
Please note that packages naming varies by distro.
## Installation
Run in terminal:
```shell
meson builddir --prefix=/usr/local
sudo meson install -C builddir
```
Additional GStreamer elements installation:
<details>
<summary>Fedora</summary>
Enable RPM Fusion and run:
```shell
sudo dnf install \
gstreamer1-plugins-base \
gstreamer1-plugins-good-gtk \
gstreamer1-libav \
gstreamer1-vaapi
```
</details>
<details>
<summary>openSUSE</summary>
```shell
sudo zypper install \
gstreamer-plugins-base \
gstreamer-plugins-good \
gstreamer-plugins-libav \
gstreamer-plugins-vaapi
```
</details>
<details>
<summary>Arch Linux</summary>
```shell
sudo pacman -S \
gst-plugins-base \
gst-plugin-gtk \
gst-libav \
gstreamer-vaapi
```
</details>
## Packages
The [pkgs folder](https://github.com/Rafostar/clapper/tree/master/pkgs) in this repository contains build scripts for various package formats. You can use them to build package yourself or download one of pre-built packages:
<details>
<summary>Fedora, openSUSE & SLE (rpm)</summary>
<summary><b>Debian, Fedora, openSUSE & Ubuntu</b></summary>
Pre-built packages are available here:<br>
[software.opensuse.org//download.html?project=home:sp1rit&package=clapper](https://software.opensuse.org//download.html?project=home%3Asp1rit&package=clapper) ([See status](https://build.opensuse.org/package/show/home:sp1rit/clapper))
Pre-built packages are available in [my repo](https://software.opensuse.org//download.html?project=home%3ARafostar&package=clapper) ([see status](https://build.opensuse.org/package/show/home:Rafostar/clapper))
</details>
<details>
<summary>Arch Linux</summary>
<summary><b>Arch Linux</b></summary>
You can get clapper from the AUR: [clapper-git](https://aur.archlinux.org/packages/clapper-git), or
You can get Clapper from the AUR: [clapper-git](https://aur.archlinux.org/packages/clapper-git), or
```shell
cd pkgs/arch
makepkg -si
```
</details>
## Installation from source code
Run in terminal:
```shell
meson builddir --prefix=/usr/local
sudo meson install -C builddir
```
GStreamer elements installation:
<details>
<summary><b>Debian/Ubuntu</b></summary>
```shell
sudo apt install \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-gl \
gstreamer1.0-gtk4 \
gstreamer1.0-libav \
gstreamer-vaapi
```
</details>
<details>
<summary><b>Fedora</b></summary>
Enable RPM Fusion and run:
```shell
sudo dnf install \
gstreamer1-plugins-base \
gstreamer1-plugins-good \
gstreamer1-plugins-good-gtk4 \
gstreamer1-plugins-bad-free \
gstreamer1-plugins-bad-free-extras \
gstreamer1-libav \
gstreamer1-vaapi
```
</details>
<details>
<summary><b>openSUSE</b></summary>
```shell
sudo zypper install \
gstreamer-plugins-base \
gstreamer-plugins-good \
gstreamer-plugins-good-gtk4 \
gstreamer-plugins-bad \
gstreamer-plugins-libav \
gstreamer-plugins-vaapi
```
</details>
<details>
<summary><b>Arch Linux</b></summary>
```shell
sudo pacman -S \
gst-plugins-base \
gst-plugins-good \
gst-plugin-gtk4 \
gst-plugins-bad-libs \
gst-libav \
gstreamer-vaapi
```
</details>
## Special Thanks
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.

View File

@@ -1,5 +1,6 @@
const { Gdk, GLib, GObject, Gtk, GstPlayer } = imports.gi;
const Debug = imports.clapper_src.debug;
const { HeaderBar } = imports.clapper_src.headerbar;
const { Interface } = imports.clapper_src.interface;
const { Player } = imports.clapper_src.player;
const { Window } = imports.clapper_src.window;
@@ -20,8 +21,6 @@ var App = GObject.registerClass({
{
_init(opts)
{
GLib.set_prgname(APP_NAME);
super._init({
application_id: pkg.name
});
@@ -39,7 +38,10 @@ var App = GObject.registerClass({
this.window = null;
this.interface = null;
this.player = null;
this.dragStartReady = false;
this.dragAllowed = false;
this.posX = 0;
this.posY = 0;
}
vfunc_startup()
@@ -51,35 +53,43 @@ var App = GObject.registerClass({
'realize', this._onWindowRealize.bind(this)
);
this.window.connect(
'key-press-event', this._onWindowKeyPressEvent.bind(this)
'fullscreen-changed', this._onWindowFullscreenChanged.bind(this)
);
this.window.connect(
'fullscreen-changed', this._onWindowFullscreenChanged.bind(this)
'close-request', this._onWindowCloseRequest.bind(this)
);
this.interface = new Interface();
let headerBar = new Gtk.HeaderBar({
title: APP_NAME,
show_close_button: true,
});
headerBar.pack_end(this.interface.controls.openMenuButton);
headerBar.pack_end(this.interface.controls.fullscreenButton);
let headerStart = [];
let headerEnd = [
this.interface.controls.openMenuButton,
this.interface.controls.fullscreenButton
];
let headerBar = new HeaderBar(this.window, headerStart, headerEnd);
this.interface.addHeaderBar(headerBar, APP_NAME);
this.interface.controls.fullscreenButton.connect(
'clicked', () => this._onInterfaceToggleFullscreenClicked(true)
'clicked', () => this.activeWindow.fullscreen()
);
this.interface.controls.unfullscreenButton.connect(
'clicked', () => this._onInterfaceToggleFullscreenClicked(false)
'clicked', () => this.activeWindow.unfullscreen()
);
this.window.set_titlebar(this.interface.headerBar);
this.window.add(this.interface);
this.window.set_child(this.interface);
}
vfunc_activate()
{
super.vfunc_activate();
this.window.show_all();
this.window.present();
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
this.cssProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
}
run(arr)
@@ -94,8 +104,8 @@ var App = GObject.registerClass({
this.hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
this.hideCursorTimeout = null;
if(this.isCursorInPlayer)
this.playerWindow.set_cursor(this.blankCursor);
if(this.player.motionController.is_pointer)
this.player.widget.set_cursor(this.blankCursor);
return GLib.SOURCE_REMOVE;
});
@@ -107,7 +117,7 @@ var App = GObject.registerClass({
this.hideControlsTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => {
this.hideControlsTimeout = null;
if(this.window.isFullscreen && this.isCursorInPlayer) {
if(this.window.isFullscreen && this.player.motionController.is_pointer) {
this.clearTimeout('updateTime');
this.interface.revealControls(false);
}
@@ -146,80 +156,63 @@ var App = GObject.registerClass({
{
this.window.disconnect(this.windowRealizeSignal);
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
this.cssProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
this.player = new Player();
this.player.widget.add_events(
Gdk.EventMask.SCROLL_MASK
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
);
this.interface.addPlayer(this.player);
this.player.connect('warning', this._onPlayerWarning.bind(this));
this.player.connect('error', this._onPlayerError.bind(this));
if(!this.player.widget)
return this.quit();
this.player.widget.width_request = 960;
this.player.widget.height_request = 540;
this.interface.addPlayer(this.player);
this.player.connect('state-changed', this._onPlayerStateChanged.bind(this));
this.player.connectWidget(
'button-press-event', this._onPlayerButtonPressEvent.bind(this)
this.player.clickGesture.connect(
'pressed', this._onPlayerPressed.bind(this)
);
this.player.connectWidget(
'enter-notify-event', this._onPlayerEnterNotifyEvent.bind(this)
this.player.keyController.connect(
'key-pressed', this._onPlayerKeyPressed.bind(this)
);
this.player.connectWidget(
'leave-notify-event', this._onPlayerLeaveNotifyEvent.bind(this)
this.player.motionController.connect(
'enter', this._onPlayerEnter.bind(this)
);
this.player.connectWidget(
'motion-notify-event', this._onPlayerMotionNotifyEvent.bind(this)
this.player.motionController.connect(
'leave', this._onPlayerLeave.bind(this)
);
this.player.motionController.connect(
'motion', this._onPlayerMotion.bind(this)
);
this.player.dragGesture.connect(
'drag-update', this._onPlayerDragUpdate.bind(this)
);
/* Widget signals that are disconnected after first run */
this._playerRealizeSignal = this.player.widget.connect(
'realize', this._onPlayerRealize.bind(this)
);
this._playerDrawSignal = this.player.widget.connect(
'draw', this._onPlayerDraw.bind(this)
this._playerMapSignal = this.player.widget.connect(
'map', this._onPlayerMap.bind(this)
);
}
_onWindowFullscreenChanged(window, isFullscreen)
{
// when changing fullscreen pango layout of popup is lost
// and we need to re-add marks to the new layout
this.interface.controls.setVolumeMarks(false);
if(isFullscreen) {
this.setUpdateTimeInterval();
this.setHideControlsTimeout();
this.interface.controls.unfullscreenButton.set_sensitive(true);
this.interface.controls.unfullscreenButton.show();
this.interface.showControls(true);
}
else {
this.clearTimeout('updateTime');
this.interface.controls.unfullscreenButton.set_sensitive(false);
this.interface.controls.unfullscreenButton.hide();
this.interface.showControls(false);
}
this.interface.setControlsOnVideo(isFullscreen);
this.interface.controls.setVolumeMarks(true);
this.interface.controls.setFullscreenMode(isFullscreen);
this.interface.setFullscreenMode(isFullscreen);
}
_onWindowKeyPressEvent(self, event)
_onPlayerKeyPressed(self, keyval, keycode, state)
{
let [res, key] = event.get_keyval();
if(!res) return;
//let keyName = Gdk.keyval_name(key);
let bool = false;
switch(key) {
switch(keyval) {
case Gdk.KEY_space:
case Gdk.KEY_Return:
this.player.toggle_play();
@@ -244,42 +237,27 @@ var App = GObject.registerClass({
break;
case Gdk.KEY_q:
case Gdk.KEY_Q:
this.window.destroy();
this._onWindowCloseRequest();
break;
default:
break;
}
}
_onInterfaceToggleFullscreenClicked(isFsRequested)
{
if(this.window.isFullscreen === isFsRequested)
return;
this.window.toggleFullscreen();
}
_onPlayerRealize()
{
this.player.widget.disconnect(this._playerRealizeSignal);
this.player.renderer.expose();
let display = this.player.widget.get_display();
this.defaultCursor = Gdk.Cursor.new_from_name('default', null);
this.blankCursor = Gdk.Cursor.new_from_name('none', null);
this.defaultCursor = Gdk.Cursor.new_from_name(
display, 'default'
);
this.blankCursor = Gdk.Cursor.new_for_display(
display, Gdk.CursorType.BLANK_CURSOR
);
this.playerWindow = this.player.widget.get_window();
this.setHideCursorTimeout();
}
_onPlayerDraw(self, data)
_onPlayerMap(self, data)
{
this.player.widget.disconnect(this._playerDrawSignal);
this.player.widget.disconnect(this._playerMapSignal);
this.emit('ready', true);
if(this.playlist.length)
@@ -291,6 +269,7 @@ var App = GObject.registerClass({
if(state === GstPlayer.PlayerState.BUFFERING)
return;
let isInhibited = false;
let flags = Gtk.ApplicationInhibitFlags.SUSPEND
| Gtk.ApplicationInhibitFlags.IDLE;
@@ -303,79 +282,66 @@ var App = GObject.registerClass({
flags,
'video is playing'
);
if(!this.inhibitCookie)
debug(new Error('could not inhibit session!'));
isInhibited = (this.inhibitCookie > 0);
}
else {
if(!this.inhibitCookie)
//if(!this.inhibitCookie)
return;
/* Uninhibit seems to be broken as of GTK 3.99.2
this.uninhibit(this.inhibitCookie);
this.inhibitCookie = null;
*/
}
debug('set prevent suspend to: ' + this.is_inhibited(flags));
debug(`set prevent suspend to: ${isInhibited}`);
}
_onPlayerButtonPressEvent(self, event)
_onPlayerPressed(gesture, nPress, x, y)
{
let [res, button] = event.get_button();
if(!res) return;
this.dragStartReady = false;
let button = gesture.get_current_button();
let isDouble = (nPress % 2 == 0);
this.dragAllowed = !isDouble;
switch(button) {
case Gdk.BUTTON_PRIMARY:
this._handlePrimaryButtonPress(event, button);
if(isDouble)
this.window.toggleFullscreen();
break;
case Gdk.BUTTON_SECONDARY:
if(event.get_event_type() === Gdk.EventType.BUTTON_PRESS)
this.player.toggle_play();
this.player.toggle_play();
break;
default:
break;
}
}
_handlePrimaryButtonPress(event, button)
_onPlayerEnter(controller, x, y)
{
let eventType = event.get_event_type();
switch(eventType) {
case Gdk.EventType.BUTTON_PRESS:
let [res, x, y] = event.get_root_coords();
if(!res)
break;
this.dragStartX = x;
this.dragStartY = y;
this.dragStartReady = true;
break;
case Gdk.EventType.DOUBLE_BUTTON_PRESS:
this.window.toggleFullscreen();
break;
default:
break;
}
}
_onPlayerEnterNotifyEvent(self, event)
{
this.isCursorInPlayer = true;
this.setHideCursorTimeout();
if(this.window.isFullscreen)
this.setHideControlsTimeout();
}
_onPlayerLeaveNotifyEvent(self, event)
_onPlayerLeave(controller)
{
this.isCursorInPlayer = false;
this.clearTimeout('hideCursor');
this.clearTimeout('hideControls');
}
_onPlayerMotionNotifyEvent(self, event)
_onPlayerMotion(self, posX, posY)
{
this.playerWindow.set_cursor(this.defaultCursor);
/* GTK4 sometimes generates motions with same coords */
if(this.posX === posX && this.posY === posY)
return;
this.posX = posX;
this.posY = posY;
this.player.widget.set_cursor(this.defaultCursor);
this.setHideCursorTimeout();
if(this.window.isFullscreen) {
@@ -388,36 +354,40 @@ var App = GObject.registerClass({
else if(this.hideControlsTimeout) {
this.clearTimeout('hideControls');
}
}
if(!this.dragStartReady || this.window.isFullscreen)
_onPlayerDragUpdate(gesture, offsetX, offsetY)
{
if(!this.dragAllowed || this.activeWindow.isFullscreen)
return;
let [res, x, y] = event.get_root_coords();
if(!res) return;
let { gtk_double_click_distance } = this.player.widget.get_settings();
let startDrag = this.player.widget.drag_check_threshold(
this.dragStartX, this.dragStartY, x, y
);
if(!startDrag) return;
if (
Math.abs(offsetX) > gtk_double_click_distance
|| Math.abs(offsetY) > gtk_double_click_distance
) {
let [isActive, startX, startY] = gesture.get_start_point();
if(!isActive) return;
this.dragStartReady = false;
let timestamp = event.get_time();
this.activeWindow.get_surface().begin_move(
gesture.get_device(),
gesture.get_current_button(),
startX,
startY,
gesture.get_current_event_time()
);
this.window.begin_move_drag(
Gdk.BUTTON_PRIMARY,
this.dragStartX,
this.dragStartY,
timestamp
);
gesture.reset();
}
}
_onPlayerWarning(self, error)
_onWindowCloseRequest()
{
debug(error.message, 'LEVEL_WARNING');
}
this.window.destroy();
this.player.widget.emit('destroy');
this.interface.emit('destroy');
_onPlayerError(self, error)
{
debug(error);
this.quit();
}
});

View File

@@ -1,41 +1,24 @@
const { GObject, Gtk } = imports.gi;
var BoxedIconButton = GObject.registerClass(
class BoxedIconButton extends Gtk.Button
var CustomButton = GObject.registerClass(
class ClapperCustomButton extends Gtk.Button
{
_init(icon, size, isFullscreen)
_init(opts)
{
super._init({
opts = opts || {};
let defaults = {
margin_top: 4,
margin_bottom: 4,
can_focus: false,
can_default: false,
});
margin_start: 1,
margin_end: 1,
};
Object.assign(opts, defaults);
this.isFullscreen = isFullscreen || false;
super._init(opts);
size = size || Gtk.IconSize.SMALL_TOOLBAR;
let image = Gtk.Image.new_from_icon_name(icon, size);
if(image)
this.set_image(image);
this.image.defaultSize = size;
this.image.fullscreenSize = (size === Gtk.IconSize.SMALL_TOOLBAR)
? Gtk.IconSize.LARGE_TOOLBAR
: Gtk.IconSize.DND;
this.get_style_context().add_class('flat');
this.box = new Gtk.Box();
this.box.pack_start(this, false, false, 0);
super.show();
}
get visible()
{
return this.box.visible;
this.isFullscreen = false;
this.add_css_class('flat');
}
setFullscreenMode(isFullscreen)
@@ -43,62 +26,126 @@ class BoxedIconButton extends Gtk.Button
if(this.isFullscreen === isFullscreen)
return;
this.image.icon_size = (isFullscreen)
? this.image.fullscreenSize
: this.image.defaultSize;
this.margin_top = (isFullscreen) ? 6 : 4;
this.isFullscreen = isFullscreen;
}
show_all()
{
this.box.show_all();
}
show()
{
this.box.show();
}
hide()
{
this.box.hide();
}
});
var BoxedPopoverButton = GObject.registerClass(
class BoxedPopoverButton extends BoxedIconButton
var IconButton = GObject.registerClass(
class ClapperIconButton extends CustomButton
{
_init(icon, size, isFullscreen)
_init(icon)
{
super._init(icon, size, isFullscreen);
this.popover = new Gtk.Popover({
relative_to: this.box
super._init({
icon_name: icon,
});
this.popoverBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL
});
this.popover.add(this.popoverBox);
this.popoverBox.show();
}
});
if(this.isFullscreen)
this.popover.get_style_context().add_class('osd');
var IconToggleButton = GObject.registerClass(
class ClapperIconToggleButton extends IconButton
{
_init(primaryIcon, secondaryIcon)
{
super._init(primaryIcon);
this.primaryIcon = primaryIcon;
this.secondaryIcon = secondaryIcon;
}
setFullscreenMode(isEnabled)
setPrimaryIcon()
{
if(this.isFullscreen === isEnabled)
this.icon_name = this.primaryIcon;
}
setSecondaryIcon()
{
this.icon_name = this.secondaryIcon;
}
});
var LabelButton = GObject.registerClass(
class ClapperLabelButton extends CustomButton
{
_init(text)
{
super._init({
margin_start: 0,
margin_end: 0,
});
this.customLabel = new Gtk.Label({
label: text,
single_line_mode: true,
});
this.customLabel.add_css_class('labelbutton');
this.set_child(this.customLabel);
}
set_label(text)
{
this.customLabel.set_text(text);
}
});
var PopoverButton = GObject.registerClass(
class ClapperPopoverButton extends IconButton
{
_init(icon)
{
super._init(icon);
this.popover = new Gtk.Popover({
position: Gtk.PositionType.TOP,
});
this.popoverBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
});
this.popover.set_parent(this);
this.popover.set_child(this.popoverBox);
this.popover.set_offset(0, -this.margin_top);
if(this.isFullscreen)
this.popover.add_css_class('osd');
this.changeStateSignal = this.popover.connect('closed', () =>
this.unset_state_flags(Gtk.StateFlags.CHECKED)
);
this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
}
setFullscreenMode(isFullscreen)
{
if(this.isFullscreen === isFullscreen)
return;
let action = (isEnabled) ? 'add_class' : 'remove_class';
this.popover.get_style_context()[action]('osd');
super.setFullscreenMode(isFullscreen);
super.setFullscreenMode(isEnabled);
this.popover.set_offset(0, -this.margin_top);
let cssClass = 'osd';
if(isFullscreen == this.popover.has_css_class(cssClass))
return;
let action = (isFullscreen) ? 'add' : 'remove';
this.popover[action + '_css_class'](cssClass);
}
vfunc_clicked()
{
this.set_state_flags(Gtk.StateFlags.CHECKED, false);
this.popover.popup();
}
_onDestroy()
{
this.disconnect(this.destroySignal);
this.popover.disconnect(this.changeStateSignal);
this.popover.unparent();
this.popoverBox.emit('destroy');
this.popover.emit('destroy');
}
});

View File

@@ -31,8 +31,8 @@ var Controls = GObject.registerClass({
valign: Gtk.Align.END,
});
this.fullscreenMode = false;
this.durationFormated = '00:00:00';
this.elapsedInitial = '00:00:00/00:00:00';
this.buttonsArr = [];
this._addTogglePlayButton();
@@ -52,142 +52,110 @@ var Controls = GObject.registerClass({
this._addVolumeButton();
this.unfullscreenButton = this.addButton(
'view-restore-symbolic',
Gtk.IconSize.SMALL_TOOLBAR,
true
);
this.unfullscreenButton.set_visible(false);
this.fullscreenButton = Gtk.Button.new_from_icon_name(
'view-fullscreen-symbolic',
Gtk.IconSize.SMALL_TOOLBAR
);
this.setDefaultWidgetBehaviour(this.fullscreenButton);
this.openMenuButton = Gtk.Button.new_from_icon_name(
'open-menu-symbolic',
Gtk.IconSize.SMALL_TOOLBAR
);
this.setDefaultWidgetBehaviour(this.openMenuButton);
this.forall(this.setDefaultWidgetBehaviour);
this.realizeSignal = this.connect(
'realize', this._onControlsRealize.bind(this)
);
}
this.add_css_class('playercontrols');
pack_start(widget, expand, fill, padding)
{
if(
widget.box
&& widget.box.constructor
&& widget.box.constructor === Gtk.Box
)
widget = widget.box;
super.pack_start(widget, expand, fill, padding);
this.realizeSignal = this.connect('realize', this._onRealize.bind(this));
this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
}
setFullscreenMode(isFullscreen)
{
if(isFullscreen === this.fullscreenMode)
return;
for(let button of this.buttonsArr)
button.setFullscreenMode(isFullscreen);
this.fullscreenMode = isFullscreen;
this.unfullscreenButton.set_visible(isFullscreen);
}
addButton(iconName, size, noPack)
addButton(buttonIcon)
{
let button = new Buttons.BoxedIconButton(
iconName, size, this.fullscreenMode
);
let button = (buttonIcon instanceof Gtk.Button)
? buttonIcon
: new Buttons.IconButton(buttonIcon);
if(!noPack)
this.pack_start(button, false, false, 0);
this.buttonsArr.push(button);
return button;
}
addPopoverButton(iconName, size)
{
let button = new Buttons.BoxedPopoverButton(
iconName, size, this.fullscreenMode
);
this.pack_start(button, false, false, 0);
this.append(button);
this.buttonsArr.push(button);
return button;
}
addRadioButtons(box, array, activeId)
addLabelButton(text)
{
text = text || '';
let button = new Buttons.LabelButton(text);
return this.addButton(button);
}
addPopoverButton(iconName)
{
let button = new Buttons.PopoverButton(iconName);
return this.addButton(button);
}
addCheckButtons(box, array, activeId)
{
let group = null;
let children = box.get_children();
let lastEl = (children.length > array.length)
? children.length
: array.length;
let child = box.get_first_child();
let i = 0;
for(let i = 0; i < lastEl; i++) {
while(child || i < array.length) {
if(i >= array.length) {
children[i].hide();
debug(`hiding unused ${children[i].type} radioButton nr: ${i}`);
child.hide();
debug(`hiding unused ${child.type} checkButton nr: ${i}`);
i++;
child = child.get_next_sibling();
continue;
}
let el = array[i];
let radioButton;
let checkButton;
if(i < children.length) {
radioButton = children[i];
debug(`reusing ${el.type} radioButton nr: ${i}`);
if(child) {
checkButton = child;
debug(`reusing ${el.type} checkButton nr: ${i}`);
}
else {
debug(`creating new ${el.type} radioButton nr: ${i}`);
radioButton = new Gtk.RadioButton({
debug(`creating new ${el.type} checkButton nr: ${i}`);
checkButton = new Gtk.CheckButton({
group: group,
});
radioButton.connect(
checkButton.connect(
'toggled',
this._onRadioButtonToggled.bind(this, radioButton)
this._onCheckButtonToggled.bind(this, checkButton)
);
this.setDefaultWidgetBehaviour(radioButton);
box.add(radioButton);
box.append(checkButton);
}
radioButton.label = el.label;
debug(`radioButton label: ${radioButton.label}`);
radioButton.type = el.type;
debug(`radioButton type: ${radioButton.type}`);
radioButton.activeId = el.activeId;
debug(`radioButton id: ${radioButton.activeId}`);
checkButton.label = el.label;
debug(`checkButton label: ${checkButton.label}`);
checkButton.type = el.type;
debug(`checkButton type: ${checkButton.type}`);
checkButton.activeId = el.activeId;
debug(`checkButton id: ${checkButton.activeId}`);
if(radioButton.activeId === activeId) {
radioButton.set_active(true);
debug(`activated ${el.type} radioButton nr: ${i}`);
if(checkButton.activeId === activeId) {
checkButton.set_active(true);
debug(`activated ${el.type} checkButton nr: ${i}`);
}
if(!group)
group = radioButton;
group = checkButton;
radioButton.show();
i++;
if(child)
child = child.get_next_sibling();
}
}
setDefaultWidgetBehaviour(widget)
{
widget.can_focus = false;
widget.can_default = false;
}
setVolumeMarks(isAdded)
{
if(!isAdded)
return this.volumeScale.clear_marks();
this.volumeScale.add_mark(0, Gtk.PositionType.LEFT, '0%');
this.volumeScale.add_mark(1, Gtk.PositionType.LEFT, '100%');
this.volumeScale.add_mark(2, Gtk.PositionType.LEFT, '200%');
}
handleScaleIncrement(type, isUp)
{
let value = this[`${type}Scale`].get_value();
@@ -206,49 +174,45 @@ var Controls = GObject.registerClass({
_addTogglePlayButton()
{
this.togglePlayButton = this.addButton(
this.togglePlayButton = new Buttons.IconToggleButton(
'media-playback-start-symbolic',
Gtk.IconSize.LARGE_TOOLBAR
'media-playback-pause-symbolic'
);
this.togglePlayButton.setPlayImage = () =>
{
this.togglePlayButton.image.set_from_icon_name(
'media-playback-start-symbolic',
this.togglePlayButton.image.icon_size
);
}
this.togglePlayButton.setPauseImage = () =>
{
this.togglePlayButton.image.set_from_icon_name(
'media-playback-pause-symbolic',
this.togglePlayButton.image.icon_size
);
}
this.togglePlayButton.add_css_class('playbackicon');
this.addButton(this.togglePlayButton);
}
_addPositionScale()
{
this.elapsedButton = this.addLabelButton(this.elapsedInitial);
this.positionScale = new Gtk.Scale({
orientation: Gtk.Orientation.HORIZONTAL,
value_pos: Gtk.PositionType.LEFT,
draw_value: true,
draw_value: false,
hexpand: true,
valign: Gtk.Align.CENTER,
can_focus: false,
});
let style = this.positionScale.get_style_context();
style.add_class('positionscale');
this.positionScale.connect(
'format-value', this._onPositionScaleFormatValue.bind(this)
this.togglePlayButton.bind_property('margin_top',
this.positionScale, 'margin_top', GObject.BindingFlags.SYNC_CREATE
);
this.positionScale.connect(
'button-press-event', this._onPositionScaleButtonPressEvent.bind(this)
this.togglePlayButton.bind_property('margin_bottom',
this.positionScale, 'margin_bottom', GObject.BindingFlags.SYNC_CREATE
);
this.positionScale.add_css_class('positionscale');
this.positionScale.connect('value-changed', this._onPositionScaleValueChanged.bind(this));
/* GTK4 is missing pressed/released signals for GtkRange/GtkScale.
* We cannot add controllers, cause it already has them, so we
* workaround this by observing css classes it currently has */
this.positionScale.connect(
'button-release-event', this._onPositionScaleButtonReleaseEvent.bind(this)
'notify::css-classes', this._onPositionScaleDragging.bind(this)
);
this.positionAdjustment = this.positionScale.get_adjustment();
this.pack_start(this.positionScale, true, true, 0);
this.append(this.positionScale);
}
_addVolumeButton()
@@ -256,10 +220,14 @@ var Controls = GObject.registerClass({
this.volumeButton = this.addPopoverButton(
'audio-volume-muted-symbolic'
);
this.volumeButton.add_events(Gdk.EventMask.SCROLL_MASK);
this.volumeButton.connect(
'scroll-event', (self, event) => this._onScrollEvent(event)
let scrollController = new Gtk.EventControllerScroll();
scrollController.set_flags(
Gtk.EventControllerScrollFlags.VERTICAL
| Gtk.EventControllerScrollFlags.DISCRETE
);
scrollController.connect('scroll', this._onScroll.bind(this));
this.volumeButton.add_controller(scrollController);
this.volumeScale = new Gtk.Scale({
orientation: Gtk.Orientation.VERTICAL,
inverted: true,
@@ -268,18 +236,18 @@ var Controls = GObject.registerClass({
round_digits: 2,
vexpand: true,
});
this.volumeScale.get_style_context().add_class('volumescale');
this.volumeScale.add_css_class('volumescale');
this.volumeAdjustment = this.volumeScale.get_adjustment();
this.volumeAdjustment.set_upper(2);
this.volumeAdjustment.set_step_increment(0.05);
this.volumeAdjustment.set_page_increment(0.05);
this.setDefaultWidgetBehaviour(this.volumeScale);
this.volumeButton.popoverBox.add(this.volumeScale);
this.volumeButton.popoverBox.show_all();
this.setVolumeMarks(true);
for(let i = 0; i <= 2; i++) {
let text = (i) ? `${i}00%` : '0%';
this.volumeScale.add_mark(i, Gtk.PositionType.LEFT, text);
}
this.volumeButton.popoverBox.append(this.volumeScale);
}
_getFormatedTime(time)
@@ -293,25 +261,25 @@ var Controls = GObject.registerClass({
return `${hours}:${minutes}:${seconds}`;
}
_onRadioButtonToggled(self, radioButton)
_onCheckButtonToggled(self, checkButton)
{
if(!radioButton.get_active())
if(!checkButton.get_active())
return;
switch(radioButton.type) {
switch(checkButton.type) {
case 'video':
case 'audio':
case 'subtitle':
this.emit(
'track-change-requested',
radioButton.type,
radioButton.activeId
checkButton.type,
checkButton.activeId
);
break;
case 'visualization':
this.emit(
`${radioButton.type}-change-requested`,
radioButton.activeId
`${checkButton.type}-change-requested`,
checkButton.activeId
);
break;
default:
@@ -319,25 +287,26 @@ var Controls = GObject.registerClass({
}
}
_onPositionScaleFormatValue(self, value)
_onPositionScaleValueChanged()
{
return this._getFormatedTime(value)
let elapsed = this._getFormatedTime(this.positionScale.get_value())
+ '/' + this.durationFormated;
this.elapsedButton.set_label(elapsed);
}
_onPositionScaleButtonPressEvent()
_onPositionScaleDragging(scale)
{
this.isPositionSeeking = true;
let isPositionSeeking = scale.has_css_class('dragging');
if(this.isPositionSeeking === isPositionSeeking)
return;
this.isPositionSeeking = isPositionSeeking;
this.emit('position-seeking-changed', this.isPositionSeeking);
}
_onPositionScaleButtonReleaseEvent()
{
this.isPositionSeeking = false;
this.emit('position-seeking-changed', this.isPositionSeeking);
}
_onControlsRealize()
_onRealize()
{
this.disconnect(this.realizeSignal);
@@ -352,27 +321,25 @@ var Controls = GObject.registerClass({
this[`${name}Button`].hide();
}
_onScrollEvent(event)
_onScroll(controller, dx, dy)
{
let [res, direction] = event.get_scroll_direction();
if(!res) return;
let isVertical = Math.abs(dy) >= Math.abs(dx);
let isIncrease = (isVertical) ? dy < 0 : dx < 0;
let type = (isVertical) ? 'volume' : 'position';
let type = 'volume';
this.handleScaleIncrement(type, isIncrease);
switch(direction) {
case Gdk.ScrollDirection.RIGHT:
case Gdk.ScrollDirection.LEFT:
type = 'position';
case Gdk.ScrollDirection.UP:
case Gdk.ScrollDirection.DOWN:
let isUp = (
direction === Gdk.ScrollDirection.UP
|| direction === Gdk.ScrollDirection.RIGHT
);
this.handleScaleIncrement(type, isUp);
break;
default:
break;
}
return true;
}
_onDestroy()
{
this.disconnect(this.destroySignal);
this.visualizationsButton.emit('destroy');
this.videoTracksButton.emit('destroy');
this.audioTracksButton.emit('destroy');
this.subtitleTracksButton.emit('destroy');
this.volumeButton.emit('destroy');
}
});

76
clapper_src/headerbar.js Normal file
View File

@@ -0,0 +1,76 @@
const { GLib, GObject, Gtk, Pango } = imports.gi;
var HeaderBar = GObject.registerClass(
class ClapperHeaderBar extends Gtk.HeaderBar
{
_init(window, startButtons, endButtons)
{
super._init({
can_focus: false,
});
this.set_title_widget(this._createWidgetForWindow(window));
startButtons.forEach(btn => this.pack_start(btn));
endButtons.forEach(btn => this.pack_end(btn));
}
updateHeaderBar(mediaInfo)
{
let title = mediaInfo.get_title();
let subtitle = mediaInfo.get_uri() || null;
if(subtitle && subtitle.startsWith('file://')) {
subtitle = GLib.path_get_basename(
GLib.filename_from_uri(subtitle)[0]
);
}
if(!title) {
title = (!subtitle)
? this.defaultTitle
: (subtitle.includes('.'))
? subtitle.split('.').slice(0, -1).join('.')
: subtitle;
subtitle = null;
}
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;
}
});

View File

@@ -19,7 +19,7 @@ class ClapperInterface extends Gtk.Grid
};
Object.assign(this, defaults, opts);
this.controlsInVideo = false;
this.fullscreenMode = false;
this.lastVolumeValue = null;
this.lastPositionValue = 0;
this.lastRevealerEventTime = 0;
@@ -33,29 +33,32 @@ class ClapperInterface extends Gtk.Grid
this.revealerBottom = new Revealers.RevealerBottom();
this.controls = new Controls();
this.videoBox.get_style_context().add_class('videobox');
this.videoBox.pack_start(this.overlay, true, true, 0);
this.videoBox.add_css_class('videobox');
this.videoBox.append(this.overlay);
this.attach(this.videoBox, 0, 0, 1, 1);
this.attach(this.controls, 0, 1, 1, 1);
this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
}
addPlayer(player)
{
this._player = player;
this._player.widget.expand = true;
this._player.widget.vexpand = true;
this._player.widget.hexpand = true;
this._player.connect('state-changed', this._onPlayerStateChanged.bind(this));
this._player.connect('volume-changed', this._onPlayerVolumeChanged.bind(this));
this._player.connect('duration-changed', this._onPlayerDurationChanged.bind(this));
this._player.connect('position-updated', this._onPlayerPositionUpdated.bind(this));
this._player.connectWidget(
'scroll-event', (self, event) => this.controls._onScrollEvent(event)
this._player.scrollController.connect(
'scroll', (ctl, dx, dy) => this.controls._onScroll(ctl, dx, dy)
);
this.controls.togglePlayButton.connect(
'clicked', this._onControlsTogglePlayClicked.bind(this)
);
this.controls.positionScale.connect(
this.scaleSig = this.controls.positionScale.connect(
'value-changed', this._onControlsPositionChanged.bind(this)
);
this.controls.volumeScale.connect(
@@ -70,9 +73,9 @@ class ClapperInterface extends Gtk.Grid
this.controls.connect(
'visualization-change-requested', this._onVisualizationChangeRequested.bind(this)
);
this.revealerTop.connect('event-after', (self, event) => this._player.widget.event(event));
//this.revealerTop.connect('event-after', (self, event) => this._player.widget.event(event));
this.overlay.add(this._player.widget);
this.overlay.set_child(this._player.widget);
this.overlay.add_overlay(this.revealerTop);
this.overlay.add_overlay(this.revealerBottom);
@@ -98,32 +101,33 @@ class ClapperInterface extends Gtk.Grid
this[`revealer${pos}`].showChild(isShow);
}
setControlsOnVideo(isOnVideo)
setFullscreenMode(isFullscreen)
{
if(this.controlsInVideo === isOnVideo)
if(this.fullscreenMode === isFullscreen)
return;
if(isOnVideo) {
if(isFullscreen) {
this.remove(this.controls);
this.controls.pack_start(this.controls.unfullscreenButton.box, false, false, 0);
this.revealerBottom.addWidget(this.controls);
this.revealerBottom.append(this.controls);
}
else {
this.revealerBottom.removeWidget(this.controls);
this.controls.remove(this.controls.unfullscreenButton.box);
this.revealerBottom.remove(this.controls);
this.attach(this.controls, 0, 1, 1, 1);
}
this.controlsInVideo = isOnVideo;
debug(`placed controls in overlay: ${isOnVideo}`);
this.controls.setFullscreenMode(isFullscreen);
this.showControls(isFullscreen);
this.fullscreenMode = isFullscreen;
debug(`interface in fullscreen mode: ${isFullscreen}`);
}
updateMediaTracks()
{
let mediaInfo = this._player.get_media_info();
// set titlebar media title and path
this.updateHeaderBar(mediaInfo);
/* Set titlebar media title and path */
this.updateTitles(mediaInfo);
// we can also check if video is "live" or "seekable" (right now unused)
// it might be a good idea to hide position seek bar and disable seeking
@@ -208,7 +212,7 @@ class ClapperInterface extends Gtk.Grid
}
continue;
}
this.controls.addRadioButtons(
this.controls.addCheckButtons(
this.controls[`${type}TracksButton`].popoverBox,
parsedInfo[`${type}Tracks`],
activeId
@@ -220,32 +224,12 @@ class ClapperInterface extends Gtk.Grid
}
}
updateHeaderBar(mediaInfo)
updateTitles(mediaInfo)
{
if(!this.headerBar)
return;
if(this.headerBar)
this.headerBar.updateHeaderBar(mediaInfo);
let title = mediaInfo.get_title();
let subtitle = mediaInfo.get_uri() || null;
if(subtitle.startsWith('file://')) {
subtitle = GLib.filename_from_uri(subtitle)[0];
subtitle = GLib.path_get_basename(subtitle);
}
if(!title) {
title = (!subtitle)
? this.defaultTitle
: (subtitle.includes('.'))
? subtitle.split('.').slice(0, -1).join('.')
: subtitle;
subtitle = null;
}
this.headerBar.set_title(title);
this.headerBar.set_subtitle(subtitle);
this.revealerTop.setMediaTitle(title);
this.revealerTop.setMediaTitle(this.headerBar.titleLabel.label);
}
updateTime()
@@ -281,7 +265,7 @@ class ClapperInterface extends Gtk.Grid
});
});
this.controls.addRadioButtons(
this.controls.addCheckButtons(
this.controls.visualizationsButton.popoverBox,
parsedVisArr,
null
@@ -300,8 +284,8 @@ class ClapperInterface extends Gtk.Grid
_onTrackChangeRequested(self, type, activeId)
{
// reenabling audio is slow (as expected),
// so it is better to toggle mute instead
/* Reenabling audio is slow (as expected),
* so it is better to toggle mute instead */
if(type === 'audio') {
if(activeId < 0)
return this._player.set_mute(true);
@@ -313,8 +297,8 @@ class ClapperInterface extends Gtk.Grid
}
if(activeId < 0) {
// disabling video leaves last frame frozen,
// so we hide it by making it transparent
/* Disabling video leaves last frame frozen,
* so we hide it by making it transparent */
if(type === 'video')
this._player.widget.set_opacity(0);
@@ -365,10 +349,10 @@ class ClapperInterface extends Gtk.Grid
case GstPlayer.PlayerState.STOPPED:
this.needsTracksUpdate = true;
case GstPlayer.PlayerState.PAUSED:
this.controls.togglePlayButton.setPlayImage();
this.controls.togglePlayButton.setPrimaryIcon();
break;
case GstPlayer.PlayerState.PLAYING:
this.controls.togglePlayButton.setPauseImage();
this.controls.togglePlayButton.setSecondaryIcon();
if(this.needsTracksUpdate) {
this.needsTracksUpdate = false;
this.updateMediaTracks();
@@ -449,7 +433,7 @@ class ClapperInterface extends Gtk.Grid
this.lastPositionValue = positionSeconds;
this._player.seek_seconds(positionSeconds);
if(this.controls.fullscreenMode)
if(this.fullscreenMode)
this.updateTime();
}
@@ -468,14 +452,10 @@ class ClapperInterface extends Gtk.Grid
: 'overamplified';
let iconName = `audio-volume-${icon}-symbolic`;
if(this.controls.volumeButton.image.icon_name !== iconName)
if(this.controls.volumeButton.icon_name !== iconName)
{
debug(`set volume icon: ${icon}`);
this.controls.volumeButton.image.set_from_icon_name(
iconName,
this.controls.volumeButton.image.icon_size
);
this.controls.volumeButton.set_icon_name(iconName);
}
if(volume === this.lastVolumeValue)
@@ -484,4 +464,10 @@ class ClapperInterface extends Gtk.Grid
this.lastVolumeValue = volume;
this._player.set_volume(volume);
}
_onDestroy()
{
this.disconnect(this.destroySignal);
this.controls.emit('destroy');
}
});

View File

@@ -1,4 +1,4 @@
const { Gio, GLib, GObject, Gst, GstPlayer } = imports.gi;
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.clapper_src.debug;
@@ -21,7 +21,16 @@ class ClapperPlayer extends GstPlayer.Player
if(!Gst.is_initialized())
Gst.init(null);
let gtkglsink = Gst.ElementFactory.make('gtkglsink', null);
let plugin = 'gtk4glsink';
let gtkglsink = Gst.ElementFactory.make(plugin, null);
if(!gtkglsink) {
return debug(new Error(
`Could not load "${plugin}".`
+ ' Do you have gstreamer-plugins-good-gtk4 installed?'
));
}
let glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtkglsink;
@@ -35,8 +44,7 @@ class ClapperPlayer extends GstPlayer.Player
video_renderer: renderer
});
// assign elements to player for later access
// and make sure that GJS will not free them early
/* Assign elements to player for later access */
this.gtkglsink = gtkglsink;
this.glsinkbin = glsinkbin;
this.dispatcher = dispatcher;
@@ -60,6 +68,7 @@ class ClapperPlayer extends GstPlayer.Player
this.set_config(config);
this.set_mute(false);
this.set_plugin_rank('vah264dec', 300);
this.loop = GLib.MainLoop.new(null, false);
this.run_loop = opts.run_loop || false;
@@ -71,9 +80,28 @@ class ClapperPlayer extends GstPlayer.Player
this._trackId = 0;
this.playlist_ext = opts.playlist_ext || 'claps';
this.keyController = new Gtk.EventControllerKey();
this.motionController = new Gtk.EventControllerMotion();
this.scrollController = new Gtk.EventControllerScroll();
this.clickGesture = new Gtk.GestureClick();
this.dragGesture = new Gtk.GestureDrag();
this.scrollController.set_flags(
Gtk.EventControllerScrollFlags.BOTH_AXES
);
this.clickGesture.set_button(0);
this.widget.add_controller(this.keyController);
this.widget.add_controller(this.motionController);
this.widget.add_controller(this.scrollController);
this.widget.add_controller(this.clickGesture);
this.widget.add_controller(this.dragGesture);
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));
this.connectWidget('destroy', this._onWidgetDestroy.bind(this));
}
@@ -241,6 +269,16 @@ class ClapperPlayer extends GstPlayer.Player
this.loop.run();
}
_onPlayerWarning(self, error)
{
debug(error.message, 'LEVEL_WARNING');
}
_onPlayerError(self, error)
{
debug(error);
}
_onWidgetDestroy()
{
while(this._widgetSignals.length)

View File

@@ -10,6 +10,13 @@ class ClapperCustomRevealer extends Gtk.Revealer
{
_init(opts)
{
opts = opts || {};
let defaults = {
visible: false,
};
Object.assign(opts, defaults);
super._init(opts);
this.revealerName = '';
@@ -19,10 +26,7 @@ class ClapperCustomRevealer extends Gtk.Revealer
{
if(isReveal) {
this._clearTimeout();
if(!this.visible)
this.show();
this._setShowTimeout();
this.set_visible(isReveal);
}
else
this._setHideTimeout();
@@ -30,68 +34,39 @@ class ClapperCustomRevealer extends Gtk.Revealer
this._timedReveal(isReveal, REVEAL_TIME);
}
show()
{
if(this.visible)
return;
// Decreased size = lower CPU usage
this._setTopAlign('START');
super.show();
debug(`showing ${this.revealerName} revealer in drawing area`);
}
hide()
{
if(!this.visible)
return;
super.hide();
debug(`removed ${this.revealerName} revealer from drawing area`);
}
showChild(isReveal)
{
this._clearTimeout();
if(isReveal)
this.show();
else if(!isReveal)
this.hide();
this.set_visible(isReveal);
this._timedReveal(isReveal, 0);
}
set_visible(isVisible)
{
if(this.visible === isVisible)
return false;
super.set_visible(isVisible);
debug(`${this.revealerName} revealer visible: ${isVisible}`);
return true;
}
_timedReveal(isReveal, time)
{
this.set_transition_duration(time);
this.set_reveal_child(isReveal);
}
// Drawing revealers on top of video frames
// increases CPU usage, so we hide them
/* Drawing revealers on top of video frames
* increases CPU usage, so we hide them */
_setHideTimeout()
{
this._clearTimeout();
this._setTopAlign('FILL');
this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => {
this._revealerTimeout = null;
this.hide();
return GLib.SOURCE_REMOVE;
});
}
_setShowTimeout()
{
this._clearTimeout();
this._setTopAlign('FILL');
this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => {
this._revealerTimeout = null;
this._setTopAlign('START');
this.set_visible(false);
return GLib.SOURCE_REMOVE;
});
@@ -105,17 +80,6 @@ class ClapperCustomRevealer extends Gtk.Revealer
GLib.source_remove(this._revealerTimeout);
this._revealerTimeout = null;
}
_setTopAlign(align)
{
if(
this.revealerName !== 'top'
|| this.valign === Gtk.Align[align]
)
return;
this.valign = Gtk.Align[align];
}
});
var RevealerTop = GObject.registerClass(
@@ -130,7 +94,7 @@ class ClapperRevealerTop extends CustomRevealer
});
this.revealerName = 'top';
/*
this.set_events(
Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
@@ -141,7 +105,7 @@ class ClapperRevealerTop extends CustomRevealer
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
);
*/
let initTime = GLib.DateTime.new_now_local().format('%X');
this.timeFormat = (initTime.length > 8)
? '%I:%M %p'
@@ -150,13 +114,13 @@ class ClapperRevealerTop extends CustomRevealer
this.revealerGrid = new Gtk.Grid({
column_spacing: 8
});
let gridContext = this.revealerGrid.get_style_context();
gridContext.add_class('osd');
gridContext.add_class('reavealertop');
this.revealerGrid.add_css_class('osd');
this.revealerGrid.add_css_class('reavealertop');
this.mediaTitle = new Gtk.Label({
ellipsize: Pango.EllipsizeMode.END,
expand: true,
vexpand: true,
hexpand: true,
margin_top: 14,
margin_start: 12,
xalign: 0,
@@ -169,17 +133,16 @@ class ClapperRevealerTop extends CustomRevealer
yalign: 0,
};
this.currentTime = new Gtk.Label(timeLabelOpts);
this.currentTime.get_style_context().add_class('osdtime');
this.currentTime.add_css_class('osdtime');
this.endTime = new Gtk.Label(timeLabelOpts);
this.endTime.get_style_context().add_class('osdendtime');
this.endTime.add_css_class('osdendtime');
this.revealerGrid.attach(this.mediaTitle, 0, 0, 1, 1);
this.revealerGrid.attach(this.currentTime, 1, 0, 1, 1);
this.revealerGrid.attach(this.endTime, 1, 0, 1, 1);
this.add(this.revealerGrid);
this.revealerGrid.show_all();
this.set_child(this.revealerGrid);
}
setMediaTitle(title)
@@ -195,8 +158,8 @@ class ClapperRevealerTop extends CustomRevealer
this.currentTime.set_label(now);
this.endTime.set_label(end);
// Make sure that next timeout is always run after clock changes,
// by delaying it for additional few milliseconds
/* Make sure that next timeout is always run after clock changes,
* by delaying it for additional few milliseconds */
let nextUpdate = 60002 - parseInt(currTime.get_seconds() * 1000);
debug(`updated current time: ${now}`);
@@ -217,19 +180,48 @@ class ClapperRevealerBottom extends CustomRevealer
this.revealerName = 'bottom';
this.revealerBox = new Gtk.Box();
this.revealerBox.get_style_context().add_class('osd');
this.revealerBox.add_css_class('osd');
this.add(this.revealerBox);
this.revealerBox.show_all();
this.set_child(this.revealerBox);
}
addWidget(widget)
append(widget)
{
this.revealerBox.pack_start(widget, false, true, 0);
this.revealerBox.append(widget);
}
removeWidget(widget)
remove(widget)
{
this.revealerBox.remove(widget);
}
set_visible(isVisible)
{
let isChange = super.set_visible(isVisible);
if(!isChange) return;
let parent = this.get_parent();
let playerWidget = parent.get_first_child();
if(!playerWidget) return;
if(isVisible) {
let box = this.get_first_child();
if(!box) return;
let controls = box.get_first_child();
if(!controls) return;
let togglePlayButton = controls.get_first_child();
if(togglePlayButton) {
togglePlayButton.grab_focus();
debug('focus moved to toggle play button');
}
playerWidget.set_can_focus(false);
}
else {
playerWidget.set_can_focus(true);
playerWidget.grab_focus();
debug('focus moved to player widget');
}
}
});

View File

@@ -13,13 +13,11 @@ var Window = GObject.registerClass({
super._init({
application: application,
title: title || 'Clapper',
border_width: 0,
resizable: true,
window_position: Gtk.WindowPosition.CENTER,
width_request: 960,
height_request: 642
destroy_with_parent: true,
});
this.isFullscreen = false;
this.mapSignal = this.connect('map', this._onMap.bind(this));
}
toggleFullscreen()
@@ -28,13 +26,10 @@ var Window = GObject.registerClass({
this[`${un}fullscreen`]();
}
vfunc_window_state_event(event)
_onStateNotify(toplevel)
{
super.vfunc_window_state_event(event);
let isFullscreen = Boolean(
event.new_window_state
& Gdk.WindowState.FULLSCREEN
toplevel.state & Gdk.ToplevelState.FULLSCREEN
);
if(this.isFullscreen === isFullscreen)
@@ -43,4 +38,12 @@ var Window = GObject.registerClass({
this.isFullscreen = isFullscreen;
this.emit('fullscreen-changed', this.isFullscreen);
}
_onMap()
{
this.disconnect(this.mapSignal);
let surface = this.get_surface();
surface.connect('notify::state', this._onStateNotify.bind(this));
}
});

View File

@@ -7,13 +7,15 @@ scale marks {
font-size: 22px;
font-weight: 500;
}
.osd .playercontrols {
-gtk-icon-size: 24px;
}
.osd button {
margin: 2px;
min-width: 36px;
min-height: 36px;
}
.osd scale trough highlight {
min-width: 6px;
min-width: 0px;
min-height: 6px;
}
.osd radio {
@@ -22,12 +24,23 @@ scale marks {
min-width: 18px;
min-height: 18px;
}
.playbackicon {
-gtk-icon-size: 20px;
}
.osd .playbackicon {
-gtk-icon-size: 28px;
}
.labelbutton {
font-family: 'Cantarell', 'Noto Sans', sans-serif;
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.videobox {
background: black;
}
.reavealertop {
min-height: 100px;
box-shadow: inset 0px 200px 10px -124px rgba(0,0,0,0.4);
min-height: 90px;
box-shadow: inset 0px 200px 10px -124px rgba(0,0,0,0.3);
font-family: 'Cantarell', 'Noto Sans', sans-serif;
font-size: 30px;
font-weight: 500;
@@ -43,25 +56,13 @@ scale marks {
font-size: 24px;
font-weight: 600;
}
/* Position Scale */
.positionscale value {
font-weight: 600;
color: currentColor;
}
.positionscale trough highlight {
min-height: 4px;
}
.osd .positionscale value {
.osd .labelbutton {
font-size: 24px;
}
.positionscale contents {
margin-left: 4px;
margin-right: 2px;
}
.osd .positionscale contents {
margin-left: 8px;
margin-right: 2px;
/* Position Scale */
.positionscale trough highlight {
min-height: 4px;
}
.osd .positionscale trough slider {
color: transparent;
@@ -75,12 +76,10 @@ scale marks {
/* Volume Scale */
.volumescale {
margin-left: 4px;
min-height: 180px;
}
.osd .volumescale {
margin: 6px;
margin-left: 10px;
min-height: 280px;
}
.volumescale marks label {
@@ -91,3 +90,6 @@ scale marks {
.osd .volumescale marks label {
margin-bottom: -8px;
}
.osd .volumescale trough highlight {
min-width: 6px;
}

View File

@@ -1,5 +1,5 @@
imports.gi.versions.Gdk = '3.0';
imports.gi.versions.Gtk = '3.0';
imports.gi.versions.Gdk = '4.0';
imports.gi.versions.Gtk = '4.0';
imports.searchPath.unshift('@importspath@');
const ClapperSrc = imports.clapper_src;

View File

@@ -1,5 +1,5 @@
imports.gi.versions.Gdk = '3.0';
imports.gi.versions.Gtk = '3.0';
imports.gi.versions.Gdk = '4.0';
imports.gi.versions.Gtk = '4.0';
const { Gst } = imports.gi;
const { App } = imports.clapper_src.app;

View File

@@ -27,13 +27,13 @@ arch=(any)
url="https://github.com/Rafostar/clapper"
license=("GPL-3.0")
depends=(
"gtk3>=3.19.4"
"gtk4>=3.99.2"
"hicolor-icon-theme"
"gjs"
"gst-plugins-base-libs"
"gst-plugins-good"
"gst-plugins-bad-libs>=1.16.0"
"gst-plugin-gtk"
"gst-plugins-bad-libs>=1.18.0"
"gst-plugin-gtk4"
)
makedepends=(
"meson>=0.50"
@@ -44,7 +44,7 @@ optdepends=(
"gst-libav: Popular video decoders"
"gstreamer-vaapi: Intel/AMD video acceleration"
)
source=("${pkgname%-git}::git+https://github.com/Rafostar/${pkgname%-git}.git#branch=master")
source=("${pkgname%-git}::git+https://github.com/Rafostar/${pkgname%-git}.git")
provides=("${pkgname%-git}")
replaces=("${pkgname%-git}")
conflicts=("${pkgname%-git}")

View File

@@ -14,14 +14,14 @@ Build-Depends: debhelper (>= 10),
Package: clapper
Architecture: all
Depends: gjs (>= 1.50),
gir1.2-gtk-3.0 (>= 3.19),
gir1.2-gtk-4.0 (>= 3.99.2),
hicolor-icon-theme,
libgstreamer1.0-0,
gstreamer1.0-plugins-base,
gstreamer1.0-plugins-good,
gstreamer1.0-plugins-bad (>= 1.16),
gstreamer1.0-plugins-bad (>= 1.18),
gstreamer1.0-gl,
gstreamer1.0-gtk3
gstreamer1.0-gtk4
Recommends: gstreamer1.0-libav,
gstreamer1.0-pulseaudio
Suggests: gstreamer-plugins-ugly,

View File

@@ -19,8 +19,8 @@
%global appname com.github.rafostar.Clapper
%global gst_version 1.16.0
%global gtk3_version 3.19.4
%global gst_version 1.18.0
%global gtk4_version 3.99.2
Name: clapper
Version: 0.0.0
@@ -39,7 +39,7 @@ BuildRequires: desktop-file-utils
BuildRequires: hicolor-icon-theme
Requires: gjs
Requires: gtk3 >= %{gtk3_version}
Requires: gtk4 >= %{gtk4_version}
Requires: hicolor-icon-theme
%if 0%{?suse_version}
@@ -51,7 +51,7 @@ BuildRequires: update-desktop-files
Requires: gstreamer
Requires: gstreamer-plugins-base
Requires: gstreamer-plugins-good
Requires: gstreamer-plugins-good-gtk
Requires: gstreamer-plugins-good-gtk4
Requires: gstreamer-plugins-bad
Requires: libgstplayer-1_0-0 >= %{gst_version}
@@ -62,14 +62,12 @@ Recommends: gstreamer-plugins-libav
Suggests: gstreamer-plugins-ugly
# Intel/AMD video acceleration
Suggests: gstreamer-plugins-vaapi
%endif
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos_version}
%else
BuildRequires: glibc-all-langpacks
Requires: gstreamer1
Requires: gstreamer1-plugins-base
Requires: gstreamer1-plugins-good
Requires: gstreamer1-plugins-good-gtk
Requires: gstreamer1-plugins-good-gtk4
# Contains GstPlayer lib
Requires: gstreamer1-plugins-bad-free >= %{gst_version}
@@ -113,7 +111,11 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_datadir}/mime/packages/%{appname}.xml
%changelog
* Wed Oct 14 2020 Rafostar <rafostar.github@gmail.com> - 0.0.0-3
- Update to GTK4
* Sat Sep 19 22:02:00 CEST 2020 sp1rit - 0.0.0-2
- Added suse_update_desktop_file macro for SuSE packages
* Fri Sep 18 2020 Rafostar <rafostar.github@gmail.com> - 0.0.0-1
- Initial package