# HG changeset patch # User Goffi # Date 1597502723 -7200 # Node ID f0648005cd11f06d975ef585a34a6bc9a371c989 # Parent 71f24ad99a2c44aa8530ddfb483668ddbfb87400 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. diff -r 71f24ad99a2c -r f0648005cd11 libervia/pages/_browser/alt_media_player.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/_browser/alt_media_player.py Sat Aug 15 16:45:23 2020 +0200 @@ -0,0 +1,217 @@ +#!/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") diff -r 71f24ad99a2c -r f0648005cd11 libervia/pages/blog/view/_browser/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/blog/view/_browser/__init__.py Sat Aug 15 16:45:23 2020 +0200 @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import alt_media_player + + +alt_media_player.install_if_needed()