# -*- 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'))