view libervia/pages/_browser/alt_media_player.py @ 1346:cda5537c71d6

browser (photos/album): videos integrations: videos can now be added to an album, the alternative media player is then used to display them. Slides options are used to remove pagination and slidebar from slideshow (they don't play well with media player), and video are reset when its slide is exited.
author Goffi <goffi@goffi.org>
date Tue, 25 Aug 2020 08:31:56 +0200
parents 472267dcd4d8
children
line wrap: on
line source

#!/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")