Mercurial > libervia-web
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") |