view libervia/pages/_browser/alt_media_player.py @ 1337:f0648005cd11

browser: alternative media player: the new `alt_media_player` module implements an `ogv.js` based media player which is able to play WebM (VP8/VP9) or Ogg (Theora, Vorbis) formats (+ other codecs not integrated yet) on browser that don't do it natively. The module imports `ogv.js` only when it's necessary (incompatible browser + videos needing it). This for now used in blog, and will be used in other places when it's suitable.
author Goffi <goffi@goffi.org>
date Sat, 15 Aug 2020 16:45:23 +0200
parents
children 472267dcd4d8
line wrap: on
line source

#!/usr/bin/env python3

"""This module implement an ogv.js based alternative media player

This is useful to play libre video/audio formats on browser that don't do it natively.
"""

from browser import document, timer


class VideoPlayer:
    TIMER_MODES = ("timer", "remaining")
    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")
        self.progress_elt.bind("click", self.on_progress_click)
        self.timer_elt = video_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")
        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)

        # buttons
        for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"):
            for elt in video_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):
        evt.stopPropagation()
        evt.target.remove()
        self.player.play()

    def on_play_click(self, evt):
        if self.player.paused:
            print("playing")
            self.player.play()
        else:
            self.player.pause()
            print("paused")

    def on_change_timer_mode_click(self, evt):
        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):
        self.player.muted = not self.player.muted

    def on_fullscreen_click(self, evt):
        try:
            fullscreen_elt = document.fullscreenElement
            request_fullscreen = self.video_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):
        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.show_controls()
        self.video_player_elt.bind("mousemove", self.on_mouse_move)

    def on_pause(self, evt):
        self.video_player_elt.classList.remove("playing")
        self.show_controls()
        self.video_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):
        if self.player.muted:
            self.video_player_elt.classList.add("muted")
        else:
            self.video_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.video_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 = ""
        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) 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
        import template
        global video_player_tpl
        video_player_tpl = template.Template("components/video_player.html")
        cls.imports_done = True

    @classmethod
    def install(cls, cant_play):
        ext_list = set()
        for data in cant_play.values():
            ext_list.update(data['ext'])
        for ori_video_elt in document.body.select('video'):
            sources = []
            src = (ori_video_elt.src or '').strip()
            if src:
                sources.append(src)

            for source_elt in ori_video_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{ori_video_elt.html}")
                continue

            try:
                ext = f".{source.rsplit('.', 1)[1]}"
            except IndexError:
                print(
                    f"No extension found for source of following elt:\n{ori_video_elt.html}")
                continue
            if ext and ext in ext_list:
                cls.do_imports()
                print(f"alternative player will be used for {source!r}")
                cls(ori_video_elt, sources)


def install_if_needed():
    CONTENT_TYPES = {
        "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".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"}

    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}"
        )
        try:
            VideoPlayer.install(cant_play)
        except NotImplementedError:
            pass
    else:
        print("This browser can play natively all requested open video/audio formats")