diff libervia/web/pages/_browser/alt_media_player.py @ 1518:eb00d593801d

refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:49:28 +0200
parents libervia/pages/_browser/alt_media_player.py@472267dcd4d8
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/web/pages/_browser/alt_media_player.py	Fri Jun 02 16:49:28 2023 +0200
@@ -0,0 +1,327 @@
+#!/usr/bin/env python3
+
+"""This module implement an alternative media player
+
+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, html
+
+
+NO_PAGINATION = "NO_PAGINATION"
+NO_SCROLLBAR = "NO_SCROLLBAR"
+
+
+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,
+            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 = media_player_elt.select_one(".timer")
+        self.timer_mode = "timer"
+
+        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
+
+        # 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 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}"))
+
+    @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()
+
+    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()
+        else:
+            self.player.pause()
+            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.media_player_elt.requestFullscreen
+        except AttributeError:
+            print("fullscreen is not available on this browser")
+        else:
+            if fullscreen_elt == None:
+                print("requesting fullscreen")
+                request_fullscreen()
+            else:
+                print(f"leaving fullscreen: {fullscreen_elt}")
+                try:
+                    document.exitFullscreen()
+                except AttributeError:
+                    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.media_player_elt.classList.add("playing")
+        self.show_controls()
+        self.media_player_elt.bind("mousemove", self.on_mouse_move)
+
+    def on_pause(self, evt):
+        self.media_player_elt.classList.remove("playing")
+        self.show_controls()
+        self.media_player_elt.unbind("mousemove")
+
+    def on_timeupdate(self, evt):
+        self.update_progress()
+
+    def on_ended(self, evt):
+        self.update_progress()
+
+    def on_volumechange(self, evt):
+        evt.stopPropagation()
+        if self.player.muted:
+            self.media_player_elt.classList.add("muted")
+        else:
+            self.media_player_elt.classList.remove("muted")
+
+    def on_mouse_move(self, evt):
+        self.show_controls()
+
+    def update_progress(self):
+        duration = self.player.duration
+        current_time = duration if self.player.ended else self.player.currentTime
+        self.progress_elt.max = duration
+        self.progress_elt.value = current_time
+        self.progress_elt.text = f"{current_time/duration*100:.02f}"
+        current_time, duration = int(current_time), int(duration)
+        if self.timer_mode == "timer":
+            cur_min, cur_sec = divmod(current_time, 60)
+            tot_min, tot_sec = divmod(duration, 60)
+            self.timer_elt.text = f"{cur_min}:{cur_sec:02d}/{tot_min}:{tot_sec:02d}"
+        elif self.timer_mode == "remaining":
+            rem_min, rem_sec = divmod(duration - current_time, 60)
+            self.timer_elt.text = f"{rem_min}:{rem_sec:02d}"
+        else:
+            print(f"ERROR: unknown timer mode: {self.timer_mode}")
+
+    def hide_controls(self):
+        self.controls_elt.classList.add("hidden")
+        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.media_player_elt.style.cursor = ""
+        if self.hide_controls_timer is not None:
+            timer.clear_timeout(self.hide_controls_timer)
+        if self.player.paused:
+            self.hide_controls_timer = None
+        else:
+            self.hide_controls_timer = timer.set_timeout(self.hide_controls, 3000)
+
+    @classmethod
+    def do_imports(cls):
+        # we do imports (notably for ogv.js) only if they are necessary
+        if cls.imports_done:
+            return
+        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 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'])
+        cls.cant_play_ext_list = ext_list
+        for to_rpl_vid_elt in document.body.select('video'):
+            sources = []
+            src = (to_rpl_vid_elt.src or '').strip()
+            if src:
+                sources.append(src)
+
+            for source_elt in to_rpl_vid_elt.select('source'):
+                src = (source_elt.src or '').strip()
+                if src:
+                    sources.append(src)
+
+            # FIXME: we only use first found source
+            try:
+                source = sources[0]
+            except IndexError:
+                print(f"Can't find any source for following elt:\n{to_rpl_vid_elt.html}")
+                continue
+
+            ext = cls.get_source_ext(source)
+
+            ext = f".{source.rsplit('.', 1)[1]}"
+            if ext is None:
+                print(
+                    "No extension found for source of following elt:\n"
+                    f"{to_rpl_vid_elt.html}"
+                )
+                continue
+            if ext in ext_list:
+                print(f"alternative player will be used for {source!r}")
+                cls(sources, to_rpl_vid_elt)
+
+
+def install_if_needed():
+    CONTENT_TYPES = {
+        "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_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 "
+            f"alternative:\n{cant_play_list}"
+        )
+        try:
+            MediaPlayer.install(cant_play)
+        except NotImplementedError:
+            pass
+    else:
+        print("This browser can play natively all requested open video/audio formats")