comparison libervia/web/pages/_browser/alt_media_player.py @ 1518:eb00d593801d

refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:49:28 +0200
parents libervia/pages/_browser/alt_media_player.py@472267dcd4d8
children
comparison
equal deleted inserted replaced
1517:b8ed9726525b 1518:eb00d593801d
1 #!/usr/bin/env python3
2
3 """This module implement an alternative media player
4
5 If browser can't play natively some libre video/audio formats, ogv.js will be used,
6 otherwise the native player will be used.
7
8 This player uses its own controls, this allow better tuning/event handling notably with
9 slideshow.
10 """
11
12 from browser import document, timer, html
13
14
15 NO_PAGINATION = "NO_PAGINATION"
16 NO_SCROLLBAR = "NO_SCROLLBAR"
17
18
19 class MediaPlayer:
20 TIMER_MODES = ("timer", "remaining")
21 # will be set to False if browser can't play natively webm or ogv
22 native = True
23 # will be set to True when template and modules will be imported
24 imports_done = False
25
26 def __init__(
27 self,
28 sources,
29 to_rpl_vid_elt=None,
30 poster=None,
31 reduce_click_area=False
32 ):
33 """
34 @param sources: list of paths to media
35 only the first one is used at the moment
36 @param to_rpl_vid_elt: video element to replace
37 if None, nothing is replaced and element must be inserted manually
38 @param reduce_click_area: when True, only center of the element will react to
39 click. Useful when used in slideshow, as click on border is used to
40 show/hide slide controls
41 """
42 self.do_imports()
43
44 self.reduce_click_area = reduce_click_area
45
46 self.media_player_elt = media_player_elt = media_player_tpl.get_elt()
47 self.player = player = self._create_player(sources, poster)
48 if to_rpl_vid_elt is not None:
49 to_rpl_vid_elt.parentNode.replaceChild(media_player_elt, to_rpl_vid_elt)
50 overlay_play_elt = self.media_player_elt.select_one(".media_overlay_play")
51 overlay_play_elt.bind("click", self.on_play_click)
52 self.progress_elt = media_player_elt.select_one("progress")
53 self.progress_elt.bind("click", self.on_progress_click)
54 self.timer_elt = media_player_elt.select_one(".timer")
55 self.timer_mode = "timer"
56
57 self.controls_elt = media_player_elt.select_one(".media_controls")
58 # we devnull 2 following events to avoid accidental side effect
59 # this is notably useful in slideshow to avoid changing the slide when
60 # the user misses slightly a button
61 self.controls_elt.bind("mousedown", self._devnull)
62 self.controls_elt.bind("click", self._devnull)
63
64 player_wrapper_elt = media_player_elt.select_one(".media_elt")
65 player.preload = "none"
66 player.src = sources[0]
67 player_wrapper_elt <= player
68 self.hide_controls_timer = None
69
70 # we capture mousedown to avoid side effect on slideshow
71 player_wrapper_elt.addEventListener("mousedown", self._devnull)
72 player_wrapper_elt.addEventListener("click", self.on_player_click)
73
74 # buttons
75 for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"):
76 for elt in media_player_elt.select(f".click_to_{handler}"):
77 elt.bind("click", getattr(self, f"on_{handler}_click"))
78 # events
79 # FIXME: progress is not implemented in OGV.js, update when available
80 for event in ("play", "pause", "timeupdate", "ended", "volumechange"):
81 player.bind(event, getattr(self, f"on_{event}"))
82
83 @property
84 def elt(self):
85 return self.media_player_elt
86
87 def _create_player(self, sources, poster):
88 """Create player element, using native one when possible"""
89 player = None
90 if not self.native:
91 source = sources[0]
92 ext = self.get_source_ext(source)
93 if ext is None:
94 print(
95 f"no extension found for {source}, using native player"
96 )
97 elif ext in self.cant_play_ext_list:
98 print(f"OGV player user for {source}")
99 player = self.ogv.OGVPlayer.new()
100 # OGCPlayer has non standard "poster" property
101 player.poster = poster
102 if player is None:
103 player = html.VIDEO(poster=poster)
104 return player
105
106 def reset(self):
107 """Put back media player in intial state
108
109 media will be stopped, time will be set to beginning, overlay will be put back
110 """
111 print("resetting media player")
112 self.player.pause()
113 self.player.currentTime = 0
114 self.media_player_elt.classList.remove("in_use")
115
116 def _devnull(self, evt):
117 # stop an event
118 evt.preventDefault()
119 evt.stopPropagation()
120
121 def on_player_click(self, evt):
122 if self.reduce_click_area:
123 bounding_rect = self.media_player_elt.getBoundingClientRect()
124 margin_x = margin_y = 200
125 if ((evt.clientX - bounding_rect.left < margin_x
126 or bounding_rect.right - evt.clientX < margin_x
127 or evt.clientY - bounding_rect.top < margin_y
128 or bounding_rect.bottom - evt.clientY < margin_y
129 )):
130 # click is not in the center, we don't handle it and let the event
131 # propagate
132 return
133 self.on_play_click(evt)
134
135 def on_play_click(self, evt):
136 evt.preventDefault()
137 evt.stopPropagation()
138 self.media_player_elt.classList.add("in_use")
139 if self.player.paused:
140 print("playing")
141 self.player.play()
142 else:
143 self.player.pause()
144 print("paused")
145
146 def on_change_timer_mode_click(self, evt):
147 evt.preventDefault()
148 evt.stopPropagation()
149 self.timer_mode = self.TIMER_MODES[
150 (self.TIMER_MODES.index(self.timer_mode) + 1) % len(self.TIMER_MODES)
151 ]
152
153 def on_change_volume_click(self, evt):
154 evt.stopPropagation()
155 self.player.muted = not self.player.muted
156
157 def on_fullscreen_click(self, evt):
158 evt.stopPropagation()
159 try:
160 fullscreen_elt = document.fullscreenElement
161 request_fullscreen = self.media_player_elt.requestFullscreen
162 except AttributeError:
163 print("fullscreen is not available on this browser")
164 else:
165 if fullscreen_elt == None:
166 print("requesting fullscreen")
167 request_fullscreen()
168 else:
169 print(f"leaving fullscreen: {fullscreen_elt}")
170 try:
171 document.exitFullscreen()
172 except AttributeError:
173 print("exitFullscreen not available on this browser")
174
175 def on_progress_click(self, evt):
176 evt.stopPropagation()
177 position = evt.offsetX / evt.target.width
178 new_time = self.player.duration * position
179 self.player.currentTime = new_time
180
181 def on_play(self, evt):
182 self.media_player_elt.classList.add("playing")
183 self.show_controls()
184 self.media_player_elt.bind("mousemove", self.on_mouse_move)
185
186 def on_pause(self, evt):
187 self.media_player_elt.classList.remove("playing")
188 self.show_controls()
189 self.media_player_elt.unbind("mousemove")
190
191 def on_timeupdate(self, evt):
192 self.update_progress()
193
194 def on_ended(self, evt):
195 self.update_progress()
196
197 def on_volumechange(self, evt):
198 evt.stopPropagation()
199 if self.player.muted:
200 self.media_player_elt.classList.add("muted")
201 else:
202 self.media_player_elt.classList.remove("muted")
203
204 def on_mouse_move(self, evt):
205 self.show_controls()
206
207 def update_progress(self):
208 duration = self.player.duration
209 current_time = duration if self.player.ended else self.player.currentTime
210 self.progress_elt.max = duration
211 self.progress_elt.value = current_time
212 self.progress_elt.text = f"{current_time/duration*100:.02f}"
213 current_time, duration = int(current_time), int(duration)
214 if self.timer_mode == "timer":
215 cur_min, cur_sec = divmod(current_time, 60)
216 tot_min, tot_sec = divmod(duration, 60)
217 self.timer_elt.text = f"{cur_min}:{cur_sec:02d}/{tot_min}:{tot_sec:02d}"
218 elif self.timer_mode == "remaining":
219 rem_min, rem_sec = divmod(duration - current_time, 60)
220 self.timer_elt.text = f"{rem_min}:{rem_sec:02d}"
221 else:
222 print(f"ERROR: unknown timer mode: {self.timer_mode}")
223
224 def hide_controls(self):
225 self.controls_elt.classList.add("hidden")
226 self.media_player_elt.style.cursor = "none"
227 if self.hide_controls_timer is not None:
228 timer.clear_timeout(self.hide_controls_timer)
229 self.hide_controls_timer = None
230
231 def show_controls(self):
232 self.controls_elt.classList.remove("hidden")
233 self.media_player_elt.style.cursor = ""
234 if self.hide_controls_timer is not None:
235 timer.clear_timeout(self.hide_controls_timer)
236 if self.player.paused:
237 self.hide_controls_timer = None
238 else:
239 self.hide_controls_timer = timer.set_timeout(self.hide_controls, 3000)
240
241 @classmethod
242 def do_imports(cls):
243 # we do imports (notably for ogv.js) only if they are necessary
244 if cls.imports_done:
245 return
246 if not cls.native:
247 from js_modules import ogv
248 cls.ogv = ogv
249 if not ogv.OGVCompat.supported('OGVPlayer'):
250 print("Can't use OGVPlayer with this browser")
251 raise NotImplementedError
252 import template
253 global media_player_tpl
254 media_player_tpl = template.Template("components/media_player.html")
255 cls.imports_done = True
256
257 @staticmethod
258 def get_source_ext(source):
259 try:
260 ext = f".{source.rsplit('.', 1)[1].strip()}"
261 except IndexError:
262 return None
263 return ext or None
264
265 @classmethod
266 def install(cls, cant_play):
267 cls.native = False
268 ext_list = set()
269 for data in cant_play.values():
270 ext_list.update(data['ext'])
271 cls.cant_play_ext_list = ext_list
272 for to_rpl_vid_elt in document.body.select('video'):
273 sources = []
274 src = (to_rpl_vid_elt.src or '').strip()
275 if src:
276 sources.append(src)
277
278 for source_elt in to_rpl_vid_elt.select('source'):
279 src = (source_elt.src or '').strip()
280 if src:
281 sources.append(src)
282
283 # FIXME: we only use first found source
284 try:
285 source = sources[0]
286 except IndexError:
287 print(f"Can't find any source for following elt:\n{to_rpl_vid_elt.html}")
288 continue
289
290 ext = cls.get_source_ext(source)
291
292 ext = f".{source.rsplit('.', 1)[1]}"
293 if ext is None:
294 print(
295 "No extension found for source of following elt:\n"
296 f"{to_rpl_vid_elt.html}"
297 )
298 continue
299 if ext in ext_list:
300 print(f"alternative player will be used for {source!r}")
301 cls(sources, to_rpl_vid_elt)
302
303
304 def install_if_needed():
305 CONTENT_TYPES = {
306 "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogv", ".ogg"]},
307 "webm_vp8": {"type": 'video/webm; codecs="vp8, vorbis"', "ext": [".webm"]},
308 "webm_vp9": {"type": 'video/webm; codecs="vp9"', "ext": [".webm"]},
309 # FIXME: handle audio
310 # "ogg_vorbis": {"type": 'audio/ogg; codecs="vorbis"', "ext": ".ogg"},
311 }
312 test_media_elt = html.VIDEO()
313 cant_play = {k:d for k,d in CONTENT_TYPES.items()
314 if test_media_elt.canPlayType(d['type']) != "probably"}
315
316 if cant_play:
317 cant_play_list = '\n'.join(f"- {k} ({d['type']})" for k, d in cant_play.items())
318 print(
319 "This browser is incompatible with following content types, using "
320 f"alternative:\n{cant_play_list}"
321 )
322 try:
323 MediaPlayer.install(cant_play)
324 except NotImplementedError:
325 pass
326 else:
327 print("This browser can play natively all requested open video/audio formats")