Source code for pympress.media_overlays.vlc_backend

# -*- coding: utf-8 -*-
#
#       media_overlays/vlc.py
#
#       Copyright 2018 Cimbali <me@cimba.li>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
#
"""
:mod:`pympress.media_overlays.vlc` -- widget to play videos using VLC
---------------------------------------------------------------------
"""

import logging
logger = logging.getLogger(__name__)

import os
import vlc
import ctypes

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib

from pympress.util import IS_WINDOWS
from pympress.media_overlays import base


[docs] def get_window_handle(window): """ Uses ctypes to call gdk_win32_window_get_handle which is not available in python gobject introspection porting. Solution from http://stackoverflow.com/a/27236258/1387346 Args: window (:class:`~Gdk.Window`): The window for which we want to get the handle Returns: The handle to the win32 window """ # get the c gpointer of the gdk window ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(window.__gpointer__, None) # get the win32 handle gdkdll = ctypes.CDLL('libgdk-3-0.dll') handle_getter = gdkdll.gdk_win32_window_get_handle handle_getter.restype = ctypes.c_void_p handle_getter.argtypes = [ctypes.c_void_p] return handle_getter(drawingarea_gpointer)
[docs] class VlcOverlay(base.VideoOverlay): """ Simple VLC widget. Its player can be controlled through the 'player' attribute, which is a :class:`~vlc.MediaPlayer` instance. """ #: A single vlc.Instance() to be shared by (possible) multiple players. _instance = None def __init__(self, *args, **kwargs): self.player = self._instance.media_player_new() # before loading UI, needed to connect "map" signal super(VlcOverlay, self).__init__(*args, **kwargs) # Simple black background painting to avoid glitching outside of video area if not self.media_type.startswith('audio'): self.movie_zone.connect('draw', self.paint_backdrop) event_manager = self.player.event_manager() event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, lambda e: GLib.idle_add(self.handle_end)) event_manager.event_attach(vlc.EventType.MediaPlayerLengthChanged, lambda e: self.update_range(self.player.get_length() / 1000. or 1.)) event_manager.event_attach(vlc.EventType.MediaPlayerTimeChanged, self.time_changed)
[docs] def handle_embed(self, mapped_widget): """ Handler to embed the VLC player in the window, connected to the :attr:`~.Gtk.Widget.signals.map` signal. """ # Do we need to be on the main thread? (especially for the mess from the win32 window handle) # assert(isinstance(threading.current_thread(), threading._MainThread)) window = self.movie_zone.get_window() if window is None: logger.error('No window in which to embed the VLC player!') return False elif IS_WINDOWS: self.player.set_hwnd(get_window_handle(window)) # get_property('window') else: self.player.set_xwindow(window.get_xid()) self.movie_zone.queue_draw() return False
[docs] def is_playing(self): """ Returns whether the media is currently playing (and not paused). Returns: `bool`: `True` iff the media is playing. """ return self.player.is_playing()
def _set_file(self, filepath): """ Sets the media file to be played by the widget. Args: filepath (`pathlib.Path`): The path to the media file path """ self.player.set_media(self._instance.media_new(filepath.resolve().as_uri()))
[docs] def handle_end(self): """ End of the stream reached: restart if looping, otherwise hide overlay Overridden because, to implement looping, vlc plugin needs to be told to start on stream end, not to seek """ if self.repeat: self.action_map.lookup_action('play').activate() else: self.action_map.lookup_action('stop').activate()
[docs] def mute(self, value): """ Mutes the player. Args: value (`bool`): `True` iff this player should be muted """ GLib.idle_add(self.player.audio_set_volume, 0 if value else 100) return False
[docs] def do_play(self): """ Start playing the media file. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ play_from_state = self.player.get_state() if play_from_state in {vlc.State.Ended, vlc.State.Playing}: self.player.stop() play_from_state = vlc.State.Stopped self.player.play() if play_from_state in {vlc.State.NothingSpecial, vlc.State.Stopped}: self.do_set_time(self.start_pos) self.movie_zone.queue_draw() return False
[docs] def paint_backdrop(self, widget, context): """ Draw behind/around the video, aka the black bars Args: widget (:class:`~Gtk.Widget`): the widget to update context (:class:`~cairo.Context`): the Cairo context (or `None` if called directly) """ context.save() context.set_source_rgb(0, 0, 0) context.fill() context.paint() context.restore()
[docs] def show(self): """ Bring the widget to the top of the overlays if necessary − also force redraw of movie zone """ super(VlcOverlay, self).show() self.movie_zone.queue_draw()
[docs] def do_play_pause(self): """ Toggle pause mode of the media. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.player.pause() if self.player.is_playing() else self.player.play() return False
[docs] def do_stop(self): """ Stops playing in the backend player. """ self.player.stop()
[docs] def time_changed(self, event): """ Handle time passing Args: event (:class:`~vlc.Event`): The event that triggered the handler """ time = self.player.get_time() / 1000. or 1. if self.last_timestamp <= self.end_pos <= time: self.handle_end() self.last_timestamp = time self.update_progress(time)
[docs] def do_set_time(self, time): """ Set the player at time `~time`. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Args: `~time` (`float`): the timestamp, in s Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ # Update last_timestamp first, as seeking should bypass auto stop after duration self.last_timestamp = time self.player.set_time(int(round(time * 1000.))) return False
[docs] @classmethod def setup_backend(cls, vlc_opts = ['--no-video-title-show']): """ Prepare/check the VLC backend. Args: vlc_opts (`list`): the arguments for starting vlc Returns: `str`: the version of VLC used by the backend """ if IS_WINDOWS and vlc.plugin_path: # let python find the DLLs os.environ['PATH'] = vlc.plugin_path + ';' + os.environ['PATH'] VlcOverlay._instance = vlc.Instance(vlc_opts) return 'VLC {}'.format(vlc.libvlc_get_version().decode('ascii'))