diff --git a/TODO.md b/TODO.md index 33fa698a..aaba4865 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ - [X] Dragging player by video (MPV) - [X] Switching video/audio/subtitles tracks from bottom bar (MPV) - [X] Over-amplification supported by default (VLC) -- [ ] Audio visualizations (VLC) +- [X] Audio visualizations (VLC) - [ ] Clock with current hour and "Ends at" time on top overlay (Kodi) - [ ] Auto select subtitles matching OS language (Totem) - [ ] Picture-in-Picture mode window diff --git a/clapper_src/controls.js b/clapper_src/controls.js index c176494d..dc568ec8 100644 --- a/clapper_src/controls.js +++ b/clapper_src/controls.js @@ -14,6 +14,9 @@ var Controls = GObject.registerClass({ 'track-change-requested': { param_types: [GObject.TYPE_STRING, GObject.TYPE_INT] }, + 'visualization-change-requested': { + param_types: [GObject.TYPE_STRING] + }, } }, class ClapperControls extends Gtk.HBox { @@ -32,6 +35,9 @@ var Controls = GObject.registerClass({ this._addTogglePlayButton(); this._addPositionScale(); + this.visualizationsButton = this.addPopoverButton( + 'display-projector-symbolic' + ); this.videoTracksButton = this.addPopoverButton( 'emblem-videos-symbolic' ); @@ -58,8 +64,11 @@ var Controls = GObject.registerClass({ Gtk.IconSize.SMALL_TOOLBAR ); this.setDefaultWidgetBehaviour(this.openMenuButton); - this.forall(this.setDefaultWidgetBehaviour); + + this.realizeSignal = this.connect( + 'realize', this._onControlsRealize.bind(this) + ); } set fullscreenMode(isFullscreen) @@ -135,7 +144,7 @@ var Controls = GObject.registerClass({ for(let i = 0; i < lastEl; i++) { if(i >= array.length) { children[i].hide(); - debug(`hiding unused ${children[i].trackType} radioButton nr: ${i}`); + debug(`hiding unused ${children[i].type} radioButton nr: ${i}`); continue; } @@ -153,19 +162,20 @@ var Controls = GObject.registerClass({ }); radioButton.connect( 'toggled', - this._onTrackRadioButtonToggled.bind(this, radioButton) + this._onRadioButtonToggled.bind(this, radioButton) ); this.setDefaultWidgetBehaviour(radioButton); box.add(radioButton); } + radioButton.label = el.label; debug(`radioButton label: ${radioButton.label}`); - radioButton.trackType = el.type; - debug(`radioButton type: ${radioButton.trackType}`); - radioButton.trackId = el.value; - debug(`radioButton track id: ${radioButton.trackId}`); + radioButton.type = el.type; + debug(`radioButton type: ${radioButton.type}`); + radioButton.activeId = el.activeId; + debug(`radioButton id: ${radioButton.activeId}`); - if(radioButton.trackId === activeId) { + if(radioButton.activeId === activeId) { radioButton.set_active(true); debug(`activated ${el.type} radioButton nr: ${i}`); } @@ -195,7 +205,7 @@ var Controls = GObject.registerClass({ _addTogglePlayButton() { this.togglePlayButton = this.addButton( - 'media-playback-pause-symbolic', + 'media-playback-start-symbolic', Gtk.IconSize.LARGE_TOOLBAR ); this.togglePlayButton.setPlayImage = () => @@ -287,16 +297,30 @@ var Controls = GObject.registerClass({ button.popover.popup(); } - _onTrackRadioButtonToggled(self, radioButton) + _onRadioButtonToggled(self, radioButton) { if(!radioButton.get_active()) return; - this.emit( - 'track-change-requested', - radioButton.trackType, - radioButton.trackId - ); + switch(radioButton.type) { + case 'video': + case 'audio': + case 'subtitle': + this.emit( + 'track-change-requested', + radioButton.type, + radioButton.activeId + ); + break; + case 'visualization': + this.emit( + `${radioButton.type}-change-requested`, + radioButton.activeId + ); + break; + default: + break; + } } _onPositionScaleFormatValue(self, value) @@ -316,4 +340,19 @@ var Controls = GObject.registerClass({ this.isPositionSeeking = false; this.emit('position-seeking-changed', this.isPositionSeeking); } + + _onControlsRealize() + { + this.disconnect(this.realizeSignal); + + let hiddenButtons = [ + 'visualizations', + 'videoTracks', + 'audioTracks', + 'subtitleTracks' + ]; + + for(let name of hiddenButtons) + this[`${name}Button`].hide(); + } }); diff --git a/clapper_src/interface.js b/clapper_src/interface.js index fb73e447..d5aebf05 100644 --- a/clapper_src/interface.js +++ b/clapper_src/interface.js @@ -67,6 +67,9 @@ class ClapperInterface extends Gtk.Grid this.controls.connect( 'track-change-requested', this._onTrackChangeRequested.bind(this) ); + this.controls.connect( + 'visualization-change-requested', this._onVisualizationChangeRequested.bind(this) + ); this.overlay.add(this._player.widget); } @@ -177,28 +180,33 @@ class ClapperInterface extends Gtk.Grid tracksArr[0] = { label: 'Disabled', type: type, - value: -1 + activeId: -1 }; } tracksArr.push({ label: text, type: type, - value: info.get_index(), + activeId: info.get_index(), }); } for(let type of ['video', 'audio', 'subtitle']) { let currStream = this._player[`get_current_${type}_track`](); let activeId = (currStream) ? currStream.get_index() : -1; - let buttonBox = this.controls[`${type}TracksButton`].get_parent(); if(currStream && type !== 'subtitle') { let caps = currStream.get_caps(); debug(`${type} caps: ${caps.to_string()}`, 'LEVEL_INFO'); } + if(type === 'video') { + let isShowVis = (parsedInfo[`${type}Tracks`].length === 0); + this.showVisualizationsButton(isShowVis); + } if(!parsedInfo[`${type}Tracks`].length) { - debug(`hiding popover button without contents: ${type}`); - buttonBox.hide(); + if(this.controls[`${type}TracksButton`].visible) { + debug(`hiding popover button without contents: ${type}`); + this.controls[`${type}TracksButton`].hide(); + } continue; } this.controls.addRadioButtons( @@ -206,7 +214,10 @@ class ClapperInterface extends Gtk.Grid parsedInfo[`${type}Tracks`], activeId ); - buttonBox.show(); + if(!this.controls[`${type}TracksButton`].visible) { + debug(`showing popover button with contents: ${type}`); + this.controls[`${type}TracksButton`].show(); + } } } @@ -237,38 +248,104 @@ class ClapperInterface extends Gtk.Grid this.headerBar.set_subtitle(subtitle); } - _onTrackChangeRequested(self, trackType, trackId) + showVisualizationsButton(isShow) + { + if(isShow && !this.controls.visualizationsButton.isVisList) { + debug('creating visualizations list'); + let visArr = GstPlayer.Player.visualizations_get(); + if(!visArr.length) + return; + + let parsedVisArr = [{ + label: 'Disabled', + type: 'visualization', + activeId: null + }]; + + visArr.forEach(vis => { + parsedVisArr.push({ + label: vis.name[0].toUpperCase() + vis.name.substring(1), + type: 'visualization', + activeId: vis.name, + }); + }); + + this.controls.addRadioButtons( + this.controls.visualizationsButton.popoverBox, + parsedVisArr, + null + ); + this.controls.visualizationsButton.isVisList = true; + debug(`total visualizations: ${visArr.length}`); + } + + if(this.controls.visualizationsButton.visible === isShow) + return debug('visualizations button is already visible'); + + let action = (isShow) ? 'show' : 'hide'; + this.controls.visualizationsButton[action](); + debug(`show visualizations button: ${isShow}`); + } + + _onTrackChangeRequested(self, type, activeId) { // reenabling audio is slow (as expected), // so it is better to toggle mute instead - if(trackType === 'audio') { - if(trackId < 0) + if(type === 'audio') { + if(activeId < 0) return this._player.set_mute(true); if(this._player.get_mute()) this._player.set_mute(false); - return this._player[`set_${trackType}_track`](trackId); + return this._player[`set_${type}_track`](activeId); } - if(trackId < 0) { + if(activeId < 0) { // disabling video leaves last frame frozen, // so we also hide the widget - if(trackType === 'video') + if(type === 'video') this._player.widget.hide(); - return this._player[`set_${trackType}_track_enabled`](false); + return this._player[`set_${type}_track_enabled`](false); } - this._player[`set_${trackType}_track`](trackId); - this._player[`set_${trackType}_track_enabled`](true); + this._player[`set_${type}_track`](activeId); + this._player[`set_${type}_track_enabled`](true); - if(trackType === 'video' && !this._player.widget.get_visible()) { + if(type === 'video' && !this._player.widget.get_visible()) { this._player.widget.show(); this._player.renderer.expose(); } } + _onVisualizationChangeRequested(self, visName) + { + let isEnabled = this._player.get_visualization_enabled(); + + if(!visName) { + if(isEnabled) { + this._player.set_visualization_enabled(false); + debug('disabled visualizations'); + } + + return; + } + + let currVis = this._player.get_current_visualization(); + + if(currVis === visName) + return; + + debug(`set visualization: ${visName}`); + this._player.set_visualization(visName); + + if(!isEnabled) { + this._player.set_visualization_enabled(true); + debug('enabled visualizations'); + } + } + _onPlayerStateChanged(player, state) { switch(state) { diff --git a/clapper_src/player.js b/clapper_src/player.js index ddf20e22..75a1c903 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -56,6 +56,7 @@ class ClapperPlayer extends GstPlayer.Player this.run_loop = opts.run_loop || false; this.widget = gtkglsink.widget; this.state = GstPlayer.PlayerState.STOPPED; + this.visualization_enabled = false; this._playlist = []; this._trackId = 0; @@ -103,6 +104,20 @@ class ClapperPlayer extends GstPlayer.Player return this._playlist; } + set_visualization_enabled(value) + { + if(value === this.visualization_enabled) + return; + + super.set_visualization_enabled(value); + this.visualization_enabled = value; + } + + get_visualization_enabled() + { + return this.visualization_enabled; + } + seek_seconds(position) { this.seek(position * 1000000000);