diff --git a/clapper_src/app.js b/clapper_src/app.js new file mode 100644 index 00000000..ac758405 --- /dev/null +++ b/clapper_src/app.js @@ -0,0 +1,62 @@ +const { GLib, GObject, Gtk } = imports.gi; +const { Player } = imports.clapper_src.player; +const { Interface } = imports.clapper_src.interface; + +const APP_NAME = 'Clapper'; + +var App = GObject.registerClass({ + Signals: { + 'player-ready': { + param_types: [GObject.TYPE_BOOLEAN] + } + } +}, class ClapperApp extends Gtk.Application +{ + _init(args) + { + GLib.set_prgname(APP_NAME); + + super._init(); + + this.connect('startup', () => this._buildUI()); + this.connect('activate', () => this._openDialog()); + } + + run(arr) + { + arr = arr || []; + super.run(arr); + } + + _buildUI() + { + this.appWindow = new Gtk.ApplicationWindow({ + application: this, + title: APP_NAME, + border_width: 0, + resizable: true, + window_position: Gtk.WindowPosition.CENTER, + width_request: 960, + height_request: 642 + }); + + this.interface = new Interface(); + + this.appWindow.add(this.interface); + this.appWindow.connect('realize', this._onRealize.bind(this)); + } + + _onRealize() + { + this.player = new Player(); + this.interface.addPlayer(this.player); + + this.player.widget.show_all(); + this.emit('player-ready', true); + } + + _openDialog() + { + this.appWindow.show_all(); + } +}); diff --git a/clapper_src/controls.js b/clapper_src/controls.js new file mode 100644 index 00000000..3cb89b92 --- /dev/null +++ b/clapper_src/controls.js @@ -0,0 +1,92 @@ +const { GObject, Gtk } = imports.gi; + +var Controls = GObject.registerClass( +class ClapperControls extends Gtk.HBox +{ + _init() + { + super._init({ + margin_top: 4, + margin_bottom: 4 + }); + + this.togglePlayButton = Gtk.Button.new_from_icon_name( + 'media-playback-pause-symbolic', + Gtk.IconSize.LARGE_TOOLBAR + ); + this.pauseButton = Gtk.Button.new_from_icon_name( + 'media-playback-start-symbolic', + Gtk.IconSize.LARGE_TOOLBAR + ); + this.playImage = this.pauseButton.image; + this.pauseImage = this.togglePlayButton.image; + + this.positionScale = new Gtk.Scale({ + orientation: Gtk.Orientation.HORIZONTAL, + value_pos: Gtk.PositionType.LEFT, + draw_value: false + }); + + this.volumeButton = new Gtk.ScaleButton({ + size: Gtk.IconSize.SMALL_TOOLBAR + }); + this._prepareVolumeButton(); + + this.pack_start(this.togglePlayButton, false, false, 4); + this.pack_start(this.positionScale, true, true, 0); + this.pack_start(this.volumeButton, false, false, 4); + } + + _prepareVolumeButton() + { + this.volumeButtonAdjustment = this.volumeButton.get_adjustment(); + + this.volumeButtonAdjustment.set_upper(2); + this.volumeButtonAdjustment.set_step_increment(0.05); + this.volumeButtonAdjustment.set_page_increment(0.05); + + let basicIcons = [ + "audio-volume-low-symbolic", + "audio-volume-medium-symbolic", + "audio-volume-medium-symbolic", + "audio-volume-high-symbolic" + ]; + + let iconsArr = [ + "audio-volume-muted-symbolic" + ]; + + for(let icon of basicIcons) + iconsArr = this._addManyToArr(icon, iconsArr, 5); + + iconsArr = this._addManyToArr( + "audio-volume-overamplified-symbolic", iconsArr, 18 + ); + + this.volumeButton.set_icons(iconsArr); + + let popup = this.volumeButton.get_popup(); + let box = popup.get_child(); + let boxChildren = box.get_children(); + + for(let child of boxChildren) { + if(child.constructor === Gtk.Button) + box.remove(child); + if(child.constructor === Gtk.Scale) { + child.height_request = 200; + child.add_mark(0, Gtk.PositionType.LEFT, '0%'); + child.add_mark(1, Gtk.PositionType.LEFT, '100%'); + child.add_mark(2, Gtk.PositionType.LEFT, '200%'); + } + } + } + + _addManyToArr(item, arr, count) + { + for(let i = 0; i < count; i++) { + arr.push(item); + } + + return arr; + } +}); diff --git a/clapper_src/interface.js b/clapper_src/interface.js new file mode 100644 index 00000000..17ed7633 --- /dev/null +++ b/clapper_src/interface.js @@ -0,0 +1,124 @@ +const { GObject, Gtk, GstPlayer } = imports.gi; +const { Controls } = imports.clapper_src.controls; + +var Interface = GObject.registerClass( +class ClapperInterface extends Gtk.Grid +{ + _init() + { + super._init(); + + this.lastPositionValue = 0; + + this.controls = new Controls(); + this.attach(this.controls, 0, 1, 1, 1); + } + + addPlayer(player) + { + this._player = player; + this._player.widget.expand = true; + this._connectControlsToPlayer(); + + this.attach(this._player.widget, 0, 0, 1, 1); + } + + _connectControlsToPlayer() + { + 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.controls.togglePlayButton.connect( + 'clicked', this._onControlsTogglePlayClicked.bind(this) + ); + this.controls.positionScale.connect( + 'value-changed', this._onControlsPositionChanged.bind(this) + ); + this.controls.volumeButton.connect( + 'value-changed', this._onControlsVolumeChanged.bind(this) + ); + } + + _onPlayerStateChanged(player, state) + { + switch(state) { + case GstPlayer.PlayerState.STOPPED: + break; + case GstPlayer.PlayerState.BUFFERING: + break; + case GstPlayer.PlayerState.PAUSED: + this.controls.togglePlayButton.image = this.controls.playImage; + break; + case GstPlayer.PlayerState.PLAYING: + this.controls.togglePlayButton.image = this.controls.pauseImage; + break; + default: + break; + } + } + + _onPlayerDurationChanged(player) + { + let duration = player.get_duration() / 1000000000; + let increment = (duration < 1) + ? 0 + : (duration < 100) + ? 1 + : duration / 100; + + let adjustment = new Gtk.Adjustment({ + upper: duration, + step_increment: increment, + page_increment: increment + }); + this.controls.positionScale.set_adjustment(adjustment); + } + + _onPlayerPositionUpdated(player, position) + { + let positionSeconds = position / 1000000000; + let positionFloor = Math.floor(positionSeconds); + + if(positionFloor === this.lastPositionValue) + return; + + this.lastPositionValue = positionFloor; + this.controls.positionScale.set_value(positionSeconds); + } + + _onPlayerVolumeChanged(player) + { + let volume = player.get_volume(); + + if(this.controls.volumeButton.value === volume) + return; + + this.controls.volumeButton.value = volume; + } + + _onControlsTogglePlayClicked() + { + this._player.toggle_play(); + } + + _onControlsPositionChanged(range) + { + let position = Math.floor(range.get_value()); + + if(position === this.lastPositionValue) + return; + + this.lastPositionValue = position; + this._player.seek_seconds(position); + } + + _onControlsVolumeChanged(widget, value) + { + if(this._player.get_volume() === value) + return; + + this._player.set_volume(value); + } +}); diff --git a/src/player.js b/clapper_src/player.js similarity index 69% rename from src/player.js rename to clapper_src/player.js index 2716627f..f12240c6 100644 --- a/src/player.js +++ b/clapper_src/player.js @@ -1,10 +1,19 @@ const { GLib, GObject, Gst, GstPlayer } = imports.gi; -var GtkPlayer = GObject.registerClass( -class GtkPlayer extends GstPlayer.Player +const DEFAULTS = { + position_update_interval: 1000, + seek_accurate: false, + user_agent: 'clapper', +}; + +var Player = GObject.registerClass( +class ClapperPlayer extends GstPlayer.Player { - _init() + _init(opts) { + opts = opts || {}; + Object.assign(opts, DEFAULTS); + let gtkglsink = Gst.ElementFactory.make('gtkglsink', null); let glsinkbin = Gst.ElementFactory.make('glsinkbin', null); glsinkbin.sink = gtkglsink; @@ -19,6 +28,13 @@ class GtkPlayer extends GstPlayer.Player video_renderer: renderer }); + let config = this.get_config(); + + for(let setting of Object.keys(DEFAULTS)) + GstPlayer.Player[`config_set_${setting}`](config, opts[setting]); + + this.set_config(config); + this.loop = GLib.MainLoop.new(null, false); this.widget = gtkglsink.widget; this.state = GstPlayer.PlayerState.STOPPED; @@ -33,6 +49,15 @@ class GtkPlayer extends GstPlayer.Player this.seek(position * 1000000000); } + toggle_play() + { + let action = (this.state === GstPlayer.PlayerState.PLAYING) + ? 'pause' + : 'play'; + + this[action](); + } + _onStateChanged(player, state) { this.state = state; diff --git a/gex.json b/gex.json index 446bc128..c75d97ee 100644 --- a/gex.json +++ b/gex.json @@ -1,6 +1,9 @@ { "name": "clapper", "files": [ - "src/player.js" + "clapper_src/app.js", + "clapper_src/controls.js", + "clapper_src/interface.js", + "clapper_src/player.js" ] } diff --git a/main.js b/main.js new file mode 100644 index 00000000..b965730d --- /dev/null +++ b/main.js @@ -0,0 +1,11 @@ +imports.gi.versions.Gtk = '3.0'; + +const { Gst } = imports.gi; +const { App } = imports.clapper_src.app; + +Gst.init(null); + +function main() +{ + let clapper = new App(arguments).run(); +}