# -*- coding: utf-8 -*-
#
# media_overlays/base.py
#
# Copyright 2015 Cimbali <me@cimba.li>
#
# Vaguely inspired from:
# gtk example/widget for VLC Python bindings
# Copyright (C) 2009-2010 the VideoLAN team
#
# 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.base` -- base widget to play videos with an unspecified backend
---------------------------------------------------------------------------------------------
"""
import logging
logger = logging.getLogger(__name__)
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gio
from pympress import document, builder
[docs]
class VideoOverlay(builder.Builder):
""" Simple Video widget.
All do_X() functions are meant to be called from the main thread, through e.g. :func:`~GLib.idle_add`,
for thread-safety in the handling of video backends.
Args:
container (:class:`~Gtk.Overlay`): The container with the slide, at the top of which we add the movie area
page_type (:class:`~pympress.document.PdfPage`): the part of the page to display
action_map (:class:`~Gio.ActionMap`): the action map that contains the actions for this media
media (:class:`~pympress.document.Media`): the object defining the properties of the video such as position etc.
"""
#: :class:`~Gtk.Overlay` that is the parent of the VideoOverlay widget.
parent = None
#: :class:`~Gtk.VBox` that contains all the elements to be overlaid.
media_overlay = None
#: A :class:`~Gtk.HBox` containing a toolbar with buttons and :attr:`~progress` the progress bar
toolbar = None
#: :class:`~Gtk.Scale` that is the progress bar in the controls toolbar - if we have one.
progress = None
#: :class:`~Gtk.DrawingArea` where the media is rendered.
movie_zone = None
#: `tuple` containing the left/top/right/bottom coordinates of the drawing area in the PDF page
relative_rect = None
#: `tuple` containing the left/top/right/bottom coordinates of the drawing area in the visible slide
rect = None
#: `bool` that tracks whether we should play automatically
autoplay = False
#: `bool` that tracks whether we should play after we finished playing
repeat = False
#: `str` representing the mime type of the media file
media_type = ''
#: `float` giving the initial starting position for playback
start_pos = 0.
#: `float` giving the end position for playback, if any
end_pos = None
#: `float` representing the last know timestamp at which the progress bar updated
last_timestamp = 0.
#: `bool` that tracks whether the user is dragging the position
dragging_position = False
#: `bool` that tracks whether the playback was paused when the user started dragging the position
dragging_paused = False
#: Format of the video time, defaults to m:ss, changed to m:ss / m:ss when the max time is known
time_format = '{:01}:{:02}'
#: `float` holding the max time in s
maxval = 1
#: :class:`~Gio.ActionMap` containing the actios for this video overlay
action_map = None
def __init__(self, container, page_type, action_map, media):
super(VideoOverlay, self).__init__()
self.parent = container
self.relative_rect = (media.x1, media.y1, media.x2, media.y2)
self.update_margins_for_page(page_type)
self.load_ui('media_overlay')
self.toolbar.set_visible(media.show_controls)
self.connect_signals(self)
# medias, here the actions are scoped to the current widget
self.action_map = action_map
self.media_overlay.insert_action_group('media', self.action_map)
if media.type:
self.media_type = media.type
else:
content_type, uncertain = Gio.content_type_guess(media.filename.as_uri())
self.media_type = Gio.content_type_get_mime_type(content_type)
self.start_pos = media.start_pos
self.end_pos = float('inf') if media.duration == 0 else media.start_pos + media.duration
self._set_file(media.filename)
self.autoplay = media.autoplay
self.repeat = media.repeat
# TODO: handle poster
[docs]
def handle_embed(self, mapped_widget):
""" Handler to embed the video player in the window, connected to the :attr:`~.Gtk.Widget.signals.map` signal.
"""
return False
[docs]
def update_range(self, max_time):
""" Update the toolbar slider size.
Args:
max_time (`float`): The maximum time in this video in s
"""
self.maxval = max_time
self.progress.set_range(0, self.maxval)
self.progress.set_increments(min(5., self.maxval / 10.), min(60., self.maxval / 10.))
sec = round(self.maxval) if self.maxval > .5 else 1.
self.time_format = '{{:01}}:{{:02}} / {:01}:{:02}'.format(*divmod(int(sec), 60))
[docs]
def update_progress(self, time):
""" Update the toolbar slider to the current time.
Args:
time (`float`): The time in this video in s
"""
self.progress.set_value(time)
[docs]
def progress_moved(self, rng, sc, val):
""" Callback to update the position of the video when the user moved the progress bar.
Args:
rng (:class:`~Gtk.Range`): The range corresponding to the scale whose position we are formatting
sc (:class:`~Gtk.Scale`): The scale whose position we are updating
val (`float`): The position of the :class:`~Gtk.Scale`, which is the number of seconds elapsed in the video
"""
return self.action_map.lookup_action('set_time').activate(GLib.Variant.new_double(val))
[docs]
def play_pause(self, *args):
""" Callback to toggle play/pausing from clicking on the DrawingArea
"""
return self.action_map.lookup_action('pause').activate()
[docs]
def handle_end(self):
""" End of the stream reached: restart if looping, otherwise hide overlay
"""
if not self.repeat:
self.action_map.lookup_action('stop').activate()
else:
self.action_map.lookup_action('set_time').activate(GLib.Variant.new_double(self.start_pos))
[docs]
def update_margins_for_page(self, page_type):
""" Recalculate the margins around the media in the event of a page type change.
Arguments:
page_type (:class:`~pympress.document.PdfPage`): the part of the page to display
"""
left, top, right, bot = page_type.to_screen(*self.relative_rect)
# Some configurations generate incorrect media positions. Assume no one intentionally puts media on notes pages.
if page_type == document.PdfPage.RIGHT and -1 <= left < right <= 0:
left, right = left + 1, right + 1
logger.warning('Shifting media from LEFT notes page to RIGHT content page')
if page_type == document.PdfPage.TOP and 1 <= top < bot <= 2:
top, bot = top - 1, bot - 1
logger.warning('Shifting media from BOTTOM notes page to TOP content page')
self.rect = left, top, right, bot
if min(self.rect) < 0 or max(self.rect) > 1:
logger.warning('Negative margin(s) clipped to 0 (might alter the aspect ratio?): ' +
'LTRB = {}'.format(self.rect))
[docs]
def resize(self):
""" Adjust the position and size of the media overlay.
"""
if not self.is_shown():
return
pw, ph = self.parent.get_allocated_width(), self.parent.get_allocated_height()
left, top, right, bot = self.rect
self.media_overlay.props.margin_left = pw * max(left, 0)
self.media_overlay.props.margin_right = pw * min(1 - right, 1)
self.media_overlay.props.margin_bottom = ph * min(1 - bot, 1)
self.media_overlay.props.margin_top = ph * max(top, 0)
[docs]
def is_shown(self):
""" Returns whether the media overlay is currently added to the overlays, or hidden.
Returns:
`bool`: `True` iff the overlay is currently displayed.
"""
return self.media_overlay.get_parent() is not None
[docs]
def is_playing(self):
""" Returns whether the media is currently playing (and not paused).
Returns:
`bool`: `True` iff the media is playing.
"""
raise NotImplementedError
[docs]
def do_stop(self):
""" Stops playing in the backend player.
"""
raise NotImplementedError
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
"""
raise NotImplementedError
[docs]
def show(self):
""" Bring the widget to the top of the overlays if necessary.
"""
if min(self.rect) < 0 or max(self.rect) > 1:
logger.warning('Negative margin(s) clipped to 0 (might alter the aspect ratio?): ' +
'LTRB = {}'.format(self.rect))
if not self.media_overlay.get_parent():
self.parent.add_overlay(self.media_overlay)
self.parent.reorder_overlay(self.media_overlay, 2)
self.resize()
self.parent.queue_draw()
self.media_overlay.show()
[docs]
def do_hide(self, *args):
""" Remove widget from overlays. Needs to be called via :func:`~GLib.idle_add`.
Returns:
`bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention)
"""
self.do_stop()
self.media_overlay.hide()
if self.media_overlay.get_parent():
self.parent.remove(self.media_overlay)
self.parent.queue_draw()
return False
[docs]
def do_play(self):
""" Start playing the media file.
Should run on the main thread to ensure we avoid reentrency problems.
Returns:
`bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention)
"""
raise NotImplementedError
[docs]
def do_play_pause(self):
""" Toggle pause mode of the media.
Should run on the main thread to ensure we avoid reentrency problems.
Returns:
`bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention)
"""
raise NotImplementedError
[docs]
def do_set_time(self, t):
""" Set the player at time t.
Should run on the main thread to ensure we avoid vlc plugins' reentrency problems.
Args:
t (`float`): the timestamp, in s
Returns:
`bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention)
"""
raise NotImplementedError