# HG changeset patch # User Goffi # Date 1598303075 -7200 # Node ID 472267dcd4d87eaee4e572c34479b6f62cb04b64 # Parent 8729d2708f6541780fc4fddf11f02cbceeb2715b 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 diff -r 8729d2708f65 -r 472267dcd4d8 libervia/pages/_browser/alt_media_player.py --- 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: