Mercurial > libervia-web
view libervia/web/pages/_browser/alt_media_player.py @ 1616:6bfeb9f0fb84
browser (calls): conferences implementation:
- Handle A/V conferences calls creation/joining by entering a conference room JID in the
search box.
- Group call box has been improved and is used both for group calls (small number of
participants) and A/V conferences (larger number of participants).
- Fullscreen button for group call is working.
- Avatar/user nickname are shown in group call on peer user, as an overlay on video
stream.
- Use `user` metadata when present to display the right user avatar/name when receiving a
stream from SFU (i.e. A/V conference).
- Peer user have a new 3 dots menu with a `pin` item to (un)pin it (i.e. display it on
full container with on top).
- Updated webrtc to handle unidirectional streams correctly and to adapt to A/V conference
specification.
rel 448
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 07 Aug 2024 00:01:57 +0200 |
parents | eb00d593801d |
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")