const { Gdk, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; const { Controls } = imports.clapper_src.controls; const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; const { Player } = imports.clapper_src.player; const Revealers = imports.clapper_src.revealers; let { debug } = Debug; var Widget = GObject.registerClass({ Signals: { 'fullscreen-changed': { param_types: [GObject.TYPE_BOOLEAN] } } }, class ClapperWidget extends Gtk.Grid { _init(opts) { 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 ); this.fullscreenMode = false; this.isSeekable = false; this.lastRevealerEventTime = 0; this.needsTracksUpdate = true; this.mediaInfoSignal = null; this.videoBox = new Gtk.Box(); this.overlay = new Gtk.Overlay(); this.revealerTop = new Revealers.RevealerTop(); this.revealerBottom = new Revealers.RevealerBottom(); this.controls = new Controls(); 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.mapSignal = this.connect('map', this._onMap.bind(this)); this.player = new Player(); this.player.widget.width_request = 960; this.player.widget.height_request = 540; this.player.selfConnect('position-updated', this._onPlayerPositionUpdated.bind(this)); this.player.selfConnect('duration-changed', this._onPlayerDurationChanged.bind(this)); this.player.selfConnect('volume-changed', this._onPlayerVolumeChanged.bind(this)); this.overlay.set_child(this.player.widget); this.overlay.add_overlay(this.revealerTop); this.overlay.add_overlay(this.revealerBottom); } revealControls(isReveal) { for(let pos of ['Top', 'Bottom']) this[`revealer${pos}`].revealChild(isReveal); } showControls(isShow) { for(let pos of ['Top', 'Bottom']) this[`revealer${pos}`].showChild(isShow); } toggleFullscreen() { let root = this.get_root(); if(!root) return; let un = (this.fullscreenMode) ? 'un' : ''; root[`${un}fullscreen`](); } _onMediaInfoUpdated(player, mediaInfo) { player.disconnect(this.mediaInfoSignal); /* Set titlebar media title and path */ this.updateTitles(mediaInfo); /* Show/hide position scale on LIVE */ let isLive = mediaInfo.is_live(); this.isSeekable = mediaInfo.is_seekable(); this.controls.setLiveMode(isLive, this.isSeekable); let streamList = mediaInfo.get_stream_list(); let parsedInfo = { videoTracks: [], audioTracks: [], subtitleTracks: [] }; for(let info of streamList) { let type, text; switch(info.constructor) { case GstPlayer.PlayerVideoInfo: type = 'video'; text = info.get_codec() + ', ' + + info.get_width() + 'x' + info.get_height(); let fps = info.get_framerate(); fps = Number((fps[0] / fps[1]).toFixed(2)); if(fps) text += `@${fps}`; break; case GstPlayer.PlayerAudioInfo: type = 'audio'; let codec = info.get_codec(); if(codec.includes('(')) { codec = codec.substring( codec.indexOf('(') + 1, codec.indexOf(')') ); } text = info.get_language() || 'Unknown'; text += ', ' + codec + ', ' + info.get_channels() + ' Channels'; break; case GstPlayer.PlayerSubtitleInfo: type = 'subtitle'; text = info.get_language() || 'Unknown'; break; default: debug(`unrecognized media info type: ${info.constructor}`); break; } let tracksArr = parsedInfo[`${type}Tracks`]; if(!tracksArr.length) { tracksArr[0] = { label: 'Disabled', type: type, activeId: -1 }; } tracksArr.push({ label: text, type: type, activeId: info.get_index(), }); } for(let type of ['video', 'audio', 'subtitle']) { let currStream = player[`get_current_${type}_track`](); let activeId = (currStream) ? currStream.get_index() : -1; 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) { if(this.controls[`${type}TracksButton`].visible) { debug(`hiding popover button without contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(false); } continue; } this.controls.addCheckButtons( this.controls[`${type}TracksButton`].popoverBox, parsedInfo[`${type}Tracks`], activeId ); if(!this.controls[`${type}TracksButton`].visible) { debug(`showing popover button with contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(true); } } this.mediaInfoSignal = null; } updateTitles(mediaInfo) { let title = mediaInfo.get_title(); let subtitle = mediaInfo.get_uri(); if(Gst.Uri.get_protocol(subtitle) === '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; } let root = this.get_root(); let headerbar = root.get_titlebar(); if(headerbar && headerbar.updateHeaderBar) headerbar.updateHeaderBar(title, subtitle); this.revealerTop.setMediaTitle(title); } updateTime() { let currTime = GLib.DateTime.new_now_local(); let endTime = currTime.add_seconds( this.controls.positionAdjustment.get_upper() - this.controls.currentPosition ); let nextUpdate = this.revealerTop.setTimes(currTime, endTime); return nextUpdate; } 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.addCheckButtons( this.controls.visualizationsButton.popoverBox, parsedVisArr, null ); this.controls.visualizationsButton.isVisList = true; debug(`total visualizations: ${visArr.length}`); } if(this.controls.visualizationsButton.visible === isShow) return; let action = (isShow) ? 'show' : 'hide'; this.controls.visualizationsButton[action](); debug(`show visualizations button: ${isShow}`); } _onPlayerStateChanged(player, state) { switch(state) { case GstPlayer.PlayerState.BUFFERING: if(!player.is_local_file) this.needsTracksUpdate = true; break; case GstPlayer.PlayerState.STOPPED: this.controls.currentPosition = 0; this.controls.positionScale.set_value(0); this.controls.togglePlayButton.setPrimaryIcon(); this.needsTracksUpdate = true; if(this.mediaInfoSignal) { player.disconnect(this.mediaInfoSignal); this.mediaInfoSignal = null; } case GstPlayer.PlayerState.PAUSED: this.controls.togglePlayButton.setPrimaryIcon(); break; case GstPlayer.PlayerState.PLAYING: this.controls.togglePlayButton.setSecondaryIcon(); if(this.needsTracksUpdate && !this.mediaInfoSignal) { this.needsTracksUpdate = false; this.mediaInfoSignal = player.connect( 'media_info_updated', this._onMediaInfoUpdated.bind(this) ); } break; default: break; } if(state === GstPlayer.PlayerState.BUFFERING) return; let window = this.get_root(); Misc.inhibitForState(state, window); } _onPlayerDurationChanged(player) { let duration = Math.floor(player.get_duration() / 1000000000); /* Sometimes GstPlayer might re-emit * duration changed during playback */ if(this.controls.currentDuration === duration) return; let increment = (duration < 1) ? 0 : (duration < 100) ? 1 : duration / 100; this.controls.positionAdjustment.set_upper(duration); this.controls.positionAdjustment.set_step_increment(increment); this.controls.positionAdjustment.set_page_increment(increment); this.controls.currentDuration = duration; this.controls.durationFormated = Misc.getFormatedTime(duration); this.controls.updateElapsedLabel(); } _onPlayerPositionUpdated(player, position) { if( !this.isSeekable || !player.seek_done || this.controls.isPositionSeeking || player.state === GstPlayer.PlayerState.BUFFERING ) return; let positionSeconds = Math.round(position / 1000000000); if(positionSeconds === this.controls.currentPosition) return; this.controls.currentPosition = positionSeconds; this.controls.positionScale.set_value(positionSeconds); } _onPlayerVolumeChanged(player) { let volume = Number(player.get_volume().toFixed(2)); if(volume === this.currentVolume) return; this.controls.currentVolume = volume; this.controls.volumeScale.set_value(volume); } _onStateNotify(toplevel) { let isFullscreen = Boolean( toplevel.state & Gdk.ToplevelState.FULLSCREEN ); if(this.fullscreenMode === isFullscreen) return; this.fullscreenMode = isFullscreen; if(isFullscreen) { this.remove(this.controls); this.revealerBottom.append(this.controls); } else { this.revealerBottom.remove(this.controls); this.attach(this.controls, 0, 1, 1, 1); } this.controls.setFullscreenMode(isFullscreen); this.showControls(isFullscreen); this.player.widget.grab_focus(); this.emit('fullscreen-changed', isFullscreen); debug(`interface in fullscreen mode: ${isFullscreen}`); } _onMap() { this.disconnect(this.mapSignal); let root = this.get_root(); if(!root) return; let surface = root.get_surface(); surface.connect('notify::state', this._onStateNotify.bind(this)); } });