changeset 1344:472267dcd4d8

browser (alt_media_player): native player support + poster + flags + restricted area: - alt_media_player will now use native player when possible. This allows to use its controls and behaviour instead of native ones. - a poster can be specified when instanciated manually - video is not preloaded anymore - handle events propagation to plays nicely when used in slideshow - a "restricted area" mode can be used to let click propagation on video border, and thus catch only play/pause in the center. This is notably useful when used in the slideshow, as border can be used to show/hide slideshow controls - player can be reset, in which case the play button overlay is put back, and video is put at its beginning - once video is played at least once, a `in_use` class is added to the element, play button overlay is removed then. This fix a bug when the overlay was still appearing when using bottom play button. - VideoPlayer has been renamed to MediaPlayer
author Goffi <goffi@goffi.org>
date Mon, 24 Aug 2020 23:04:35 +0200
parents 8729d2708f65
children ce1217e3a9c0
files libervia/pages/_browser/alt_media_player.py
diffstat 1 files changed, 164 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/pages/_browser/alt_media_player.py	Mon Aug 24 22:53:15 2020 +0200
+++ b/libervia/pages/_browser/alt_media_player.py	Mon Aug 24 23:04:35 2020 +0200
@@ -1,51 +1,141 @@
 #!/usr/bin/env python3
 
-"""This module implement an ogv.js based alternative media player
+"""This module implement an alternative media player
 
-This is useful to play libre video/audio formats on browser that don't do it natively.
+If browser can't play natively some libre video/audio formats, ogv.js will be used,
+otherwise the native player will be used.
+
+This player uses its own controls, this allow better tuning/event handling notably with
+slideshow.
 """
 
-from browser import document, timer
+from browser import document, timer, html
+
+
+NO_PAGINATION = "NO_PAGINATION"
+NO_SCROLLBAR = "NO_SCROLLBAR"
 
 
-class VideoPlayer:
+class MediaPlayer:
     TIMER_MODES = ("timer", "remaining")
+    # will be set to False if browser can't play natively webm or ogv
+    native = True
+    # will be set to True when template and modules will be imported
     imports_done = False
 
-    def __init__(self, ori_video_elt, sources):
-        self.video_player_elt = video_player_elt = video_player_tpl.get_elt()
-        self.player = player = self.ogv.OGVPlayer.new() # {"debug": True})
-        ori_video_elt.parentNode.replaceChild(video_player_elt, ori_video_elt)
-        overlay_play_elt = self.video_player_elt.select_one(".video_overlay_play")
-        overlay_play_elt.bind("click", self.on_overlay_play_elt_click)
-        self.progress_elt = video_player_elt.select_one("progress")
+    def __init__(
+            self,
+            sources,
+            to_rpl_vid_elt=None,
+            poster=None,
+            reduce_click_area=False
+    ):
+        """
+        @param sources: list of paths to media
+            only the first one is used at the moment
+        @param to_rpl_vid_elt: video element to replace
+            if None, nothing is replaced and element must be inserted manually
+        @param reduce_click_area: when True, only center of the element will react to
+            click. Useful when used in slideshow, as click on border is used to
+            show/hide slide controls
+        """
+        self.do_imports()
+
+        self.reduce_click_area = reduce_click_area
+
+        self.media_player_elt = media_player_elt = media_player_tpl.get_elt()
+        self.player = player = self._create_player(sources, poster)
+        if to_rpl_vid_elt is not None:
+            to_rpl_vid_elt.parentNode.replaceChild(media_player_elt, to_rpl_vid_elt)
+        overlay_play_elt = self.media_player_elt.select_one(".media_overlay_play")
+        overlay_play_elt.bind("click", self.on_play_click)
+        self.progress_elt = media_player_elt.select_one("progress")
         self.progress_elt.bind("click", self.on_progress_click)
-        self.timer_elt = video_player_elt.select_one(".timer")
+        self.timer_elt = media_player_elt.select_one(".timer")
         self.timer_mode = "timer"
-        self.controls_elt = video_player_elt.select_one(".video_controls")
-        player_wrapper_elt = video_player_elt.select_one(".video_elt")
+
+        self.controls_elt = media_player_elt.select_one(".media_controls")
+        # we devnull 2 following events to avoid accidental side effect
+        # this is notably useful in slideshow to avoid changing the slide when
+        # the user misses slightly a button
+        self.controls_elt.bind("mousedown", self._devnull)
+        self.controls_elt.bind("click", self._devnull)
+
+        player_wrapper_elt = media_player_elt.select_one(".media_elt")
+        player.preload = "none"
         player.src = sources[0]
         player_wrapper_elt <= player
         self.hide_controls_timer = None
 
-        # a click on the video itself is like click on play icon
-        player_wrapper_elt.bind("click", self.on_play_click)
+        # we capture mousedown to avoid side effect on slideshow
+        player_wrapper_elt.addEventListener("mousedown", self._devnull)
+        player_wrapper_elt.addEventListener("click", self.on_player_click)
 
         # buttons
         for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"):
-            for elt in video_player_elt.select(f".click_to_{handler}"):
+            for elt in media_player_elt.select(f".click_to_{handler}"):
                 elt.bind("click", getattr(self, f"on_{handler}_click"))
         # events
         # FIXME: progress is not implemented in OGV.js, update when available
         for event in ("play", "pause", "timeupdate", "ended", "volumechange"):
             player.bind(event, getattr(self, f"on_{event}"))
 
-    def on_overlay_play_elt_click(self, evt):
+    @property
+    def elt(self):
+        return self.media_player_elt
+
+    def _create_player(self, sources, poster):
+        """Create player element, using native one when possible"""
+        player = None
+        if not self.native:
+            source = sources[0]
+            ext = self.get_source_ext(source)
+            if ext is None:
+                print(
+                    f"no extension found for {source}, using native player"
+                )
+            elif ext in self.cant_play_ext_list:
+                print(f"OGV player user for {source}")
+                player = self.ogv.OGVPlayer.new()
+                # OGCPlayer has non standard "poster" property
+                player.poster = poster
+        if player is None:
+            player = html.VIDEO(poster=poster)
+        return player
+
+    def reset(self):
+        """Put back media player in intial state
+
+        media will be stopped, time will be set to beginning, overlay will be put back
+        """
+        print("resetting media player")
+        self.player.pause()
+        self.player.currentTime = 0
+        self.media_player_elt.classList.remove("in_use")
+
+    def _devnull(self, evt):
+        # stop an event
+        evt.preventDefault()
         evt.stopPropagation()
-        evt.target.remove()
-        self.player.play()
+
+    def on_player_click(self, evt):
+        if self.reduce_click_area:
+            bounding_rect = self.media_player_elt.getBoundingClientRect()
+            margin_x = margin_y = 200
+            if ((evt.clientX - bounding_rect.left < margin_x
+                 or bounding_rect.right - evt.clientX < margin_x
+                 or evt.clientY - bounding_rect.top < margin_y
+                 or bounding_rect.bottom - evt.clientY < margin_y
+               )):
+                # click is not in the center, we don't handle it and let the event
+                # propagate
+                return
+        self.on_play_click(evt)
 
     def on_play_click(self, evt):
+        evt.preventDefault()
+        evt.stopPropagation()
+        self.media_player_elt.classList.add("in_use")
         if self.player.paused:
             print("playing")
             self.player.play()
@@ -54,17 +144,21 @@
             print("paused")
 
     def on_change_timer_mode_click(self, evt):
+        evt.preventDefault()
+        evt.stopPropagation()
         self.timer_mode = self.TIMER_MODES[
             (self.TIMER_MODES.index(self.timer_mode) + 1) % len(self.TIMER_MODES)
         ]
 
     def on_change_volume_click(self, evt):
+        evt.stopPropagation()
         self.player.muted = not self.player.muted
 
     def on_fullscreen_click(self, evt):
+        evt.stopPropagation()
         try:
             fullscreen_elt = document.fullscreenElement
-            request_fullscreen = self.video_player_elt.requestFullscreen
+            request_fullscreen = self.media_player_elt.requestFullscreen
         except AttributeError:
             print("fullscreen is not available on this browser")
         else:
@@ -79,19 +173,20 @@
                     print("exitFullscreen not available on this browser")
 
     def on_progress_click(self, evt):
+        evt.stopPropagation()
         position = evt.offsetX / evt.target.width
         new_time = self.player.duration * position
         self.player.currentTime = new_time
 
     def on_play(self, evt):
-        self.video_player_elt.classList.add("playing")
+        self.media_player_elt.classList.add("playing")
         self.show_controls()
-        self.video_player_elt.bind("mousemove", self.on_mouse_move)
+        self.media_player_elt.bind("mousemove", self.on_mouse_move)
 
     def on_pause(self, evt):
-        self.video_player_elt.classList.remove("playing")
+        self.media_player_elt.classList.remove("playing")
         self.show_controls()
-        self.video_player_elt.unbind("mousemove")
+        self.media_player_elt.unbind("mousemove")
 
     def on_timeupdate(self, evt):
         self.update_progress()
@@ -100,10 +195,11 @@
         self.update_progress()
 
     def on_volumechange(self, evt):
+        evt.stopPropagation()
         if self.player.muted:
-            self.video_player_elt.classList.add("muted")
+            self.media_player_elt.classList.add("muted")
         else:
-            self.video_player_elt.classList.remove("muted")
+            self.media_player_elt.classList.remove("muted")
 
     def on_mouse_move(self, evt):
         self.show_controls()
@@ -127,14 +223,14 @@
 
     def hide_controls(self):
         self.controls_elt.classList.add("hidden")
-        self.video_player_elt.style.cursor = "none"
+        self.media_player_elt.style.cursor = "none"
         if self.hide_controls_timer is not None:
             timer.clear_timeout(self.hide_controls_timer)
             self.hide_controls_timer = None
 
     def show_controls(self):
         self.controls_elt.classList.remove("hidden")
-        self.video_player_elt.style.cursor = ""
+        self.media_player_elt.style.cursor = ""
         if self.hide_controls_timer is not None:
             timer.clear_timeout(self.hide_controls_timer)
         if self.player.paused:
@@ -144,31 +240,42 @@
 
     @classmethod
     def do_imports(cls):
-        # we do imports (notably for ogv.js) if they are necessary
+        # we do imports (notably for ogv.js) only if they are necessary
         if cls.imports_done:
             return
-        from js_modules import ogv
-        cls.ogv = ogv
-        if not ogv.OGVCompat.supported('OGVPlayer'):
-            print("Can't use OGVPlayer with this browser")
-            raise NotImplementedError
+        if not cls.native:
+            from js_modules import ogv
+            cls.ogv = ogv
+            if not ogv.OGVCompat.supported('OGVPlayer'):
+                print("Can't use OGVPlayer with this browser")
+                raise NotImplementedError
         import template
-        global video_player_tpl
-        video_player_tpl = template.Template("components/video_player.html")
+        global media_player_tpl
+        media_player_tpl = template.Template("components/media_player.html")
         cls.imports_done = True
 
+    @staticmethod
+    def get_source_ext(source):
+        try:
+            ext = f".{source.rsplit('.', 1)[1].strip()}"
+        except IndexError:
+            return None
+        return ext or None
+
     @classmethod
     def install(cls, cant_play):
+        cls.native = False
         ext_list = set()
         for data in cant_play.values():
             ext_list.update(data['ext'])
-        for ori_video_elt in document.body.select('video'):
+        cls.cant_play_ext_list = ext_list
+        for to_rpl_vid_elt in document.body.select('video'):
             sources = []
-            src = (ori_video_elt.src or '').strip()
+            src = (to_rpl_vid_elt.src or '').strip()
             if src:
                 sources.append(src)
 
-            for source_elt in ori_video_elt.select('source'):
+            for source_elt in to_rpl_vid_elt.select('source'):
                 src = (source_elt.src or '').strip()
                 if src:
                     sources.append(src)
@@ -177,40 +284,43 @@
             try:
                 source = sources[0]
             except IndexError:
-                print(f"Can't find any source for following elt:\n{ori_video_elt.html}")
+                print(f"Can't find any source for following elt:\n{to_rpl_vid_elt.html}")
                 continue
 
-            try:
-                ext = f".{source.rsplit('.', 1)[1]}"
-            except IndexError:
+            ext = cls.get_source_ext(source)
+
+            ext = f".{source.rsplit('.', 1)[1]}"
+            if ext is None:
                 print(
-                    f"No extension found for source of following elt:\n{ori_video_elt.html}")
+                    "No extension found for source of following elt:\n"
+                    f"{to_rpl_vid_elt.html}"
+                )
                 continue
-            if ext and ext in ext_list:
-                cls.do_imports()
+            if ext in ext_list:
                 print(f"alternative player will be used for {source!r}")
-                cls(ori_video_elt, sources)
+                cls(sources, to_rpl_vid_elt)
 
 
 def install_if_needed():
     CONTENT_TYPES = {
-        "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogg"]},
+        "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogv", ".ogg"]},
         "webm_vp8": {"type": 'video/webm; codecs="vp8, vorbis"', "ext": [".webm"]},
         "webm_vp9": {"type": 'video/webm; codecs="vp9"', "ext": [".webm"]},
         # FIXME: handle audio
         # "ogg_vorbis": {"type": 'audio/ogg; codecs="vorbis"', "ext": ".ogg"},
     }
-    test_video_elt = document.createElement("video")
-    cant_play = {k:d for k,d in CONTENT_TYPES.items() if test_video_elt.canPlayType(d['type']) != "probably"}
+    test_media_elt = html.VIDEO()
+    cant_play = {k:d for k,d in CONTENT_TYPES.items()
+                 if test_media_elt.canPlayType(d['type']) != "probably"}
 
     if cant_play:
         cant_play_list = '\n'.join(f"- {k} ({d['type']})" for k, d in cant_play.items())
         print(
-            "This browser is incompatible with following content types, using alternative:\n"
-            f"{cant_play_list}"
+            "This browser is incompatible with following content types, using "
+            f"alternative:\n{cant_play_list}"
         )
         try:
-            VideoPlayer.install(cant_play)
+            MediaPlayer.install(cant_play)
         except NotImplementedError:
             pass
     else: