Mercurial > libervia-web
comparison libervia/pages/_browser/alt_media_player.py @ 1344:472267dcd4d8
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
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 24 Aug 2020 23:04:35 +0200 |
parents | f0648005cd11 |
children |
comparison
equal
deleted
inserted
replaced
1343:8729d2708f65 | 1344:472267dcd4d8 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 """This module implement an ogv.js based alternative media player | 3 """This module implement an alternative media player |
4 | 4 |
5 This is useful to play libre video/audio formats on browser that don't do it natively. | 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. | |
6 """ | 10 """ |
7 | 11 |
8 from browser import document, timer | 12 from browser import document, timer, html |
9 | 13 |
10 | 14 |
11 class VideoPlayer: | 15 NO_PAGINATION = "NO_PAGINATION" |
16 NO_SCROLLBAR = "NO_SCROLLBAR" | |
17 | |
18 | |
19 class MediaPlayer: | |
12 TIMER_MODES = ("timer", "remaining") | 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 | |
13 imports_done = False | 24 imports_done = False |
14 | 25 |
15 def __init__(self, ori_video_elt, sources): | 26 def __init__( |
16 self.video_player_elt = video_player_elt = video_player_tpl.get_elt() | 27 self, |
17 self.player = player = self.ogv.OGVPlayer.new() # {"debug": True}) | 28 sources, |
18 ori_video_elt.parentNode.replaceChild(video_player_elt, ori_video_elt) | 29 to_rpl_vid_elt=None, |
19 overlay_play_elt = self.video_player_elt.select_one(".video_overlay_play") | 30 poster=None, |
20 overlay_play_elt.bind("click", self.on_overlay_play_elt_click) | 31 reduce_click_area=False |
21 self.progress_elt = video_player_elt.select_one("progress") | 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") | |
22 self.progress_elt.bind("click", self.on_progress_click) | 53 self.progress_elt.bind("click", self.on_progress_click) |
23 self.timer_elt = video_player_elt.select_one(".timer") | 54 self.timer_elt = media_player_elt.select_one(".timer") |
24 self.timer_mode = "timer" | 55 self.timer_mode = "timer" |
25 self.controls_elt = video_player_elt.select_one(".video_controls") | 56 |
26 player_wrapper_elt = video_player_elt.select_one(".video_elt") | 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" | |
27 player.src = sources[0] | 66 player.src = sources[0] |
28 player_wrapper_elt <= player | 67 player_wrapper_elt <= player |
29 self.hide_controls_timer = None | 68 self.hide_controls_timer = None |
30 | 69 |
31 # a click on the video itself is like click on play icon | 70 # we capture mousedown to avoid side effect on slideshow |
32 player_wrapper_elt.bind("click", self.on_play_click) | 71 player_wrapper_elt.addEventListener("mousedown", self._devnull) |
72 player_wrapper_elt.addEventListener("click", self.on_player_click) | |
33 | 73 |
34 # buttons | 74 # buttons |
35 for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"): | 75 for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"): |
36 for elt in video_player_elt.select(f".click_to_{handler}"): | 76 for elt in media_player_elt.select(f".click_to_{handler}"): |
37 elt.bind("click", getattr(self, f"on_{handler}_click")) | 77 elt.bind("click", getattr(self, f"on_{handler}_click")) |
38 # events | 78 # events |
39 # FIXME: progress is not implemented in OGV.js, update when available | 79 # FIXME: progress is not implemented in OGV.js, update when available |
40 for event in ("play", "pause", "timeupdate", "ended", "volumechange"): | 80 for event in ("play", "pause", "timeupdate", "ended", "volumechange"): |
41 player.bind(event, getattr(self, f"on_{event}")) | 81 player.bind(event, getattr(self, f"on_{event}")) |
42 | 82 |
43 def on_overlay_play_elt_click(self, evt): | 83 @property |
44 evt.stopPropagation() | 84 def elt(self): |
45 evt.target.remove() | 85 return self.media_player_elt |
46 self.player.play() | 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) | |
47 | 134 |
48 def on_play_click(self, evt): | 135 def on_play_click(self, evt): |
136 evt.preventDefault() | |
137 evt.stopPropagation() | |
138 self.media_player_elt.classList.add("in_use") | |
49 if self.player.paused: | 139 if self.player.paused: |
50 print("playing") | 140 print("playing") |
51 self.player.play() | 141 self.player.play() |
52 else: | 142 else: |
53 self.player.pause() | 143 self.player.pause() |
54 print("paused") | 144 print("paused") |
55 | 145 |
56 def on_change_timer_mode_click(self, evt): | 146 def on_change_timer_mode_click(self, evt): |
147 evt.preventDefault() | |
148 evt.stopPropagation() | |
57 self.timer_mode = self.TIMER_MODES[ | 149 self.timer_mode = self.TIMER_MODES[ |
58 (self.TIMER_MODES.index(self.timer_mode) + 1) % len(self.TIMER_MODES) | 150 (self.TIMER_MODES.index(self.timer_mode) + 1) % len(self.TIMER_MODES) |
59 ] | 151 ] |
60 | 152 |
61 def on_change_volume_click(self, evt): | 153 def on_change_volume_click(self, evt): |
154 evt.stopPropagation() | |
62 self.player.muted = not self.player.muted | 155 self.player.muted = not self.player.muted |
63 | 156 |
64 def on_fullscreen_click(self, evt): | 157 def on_fullscreen_click(self, evt): |
158 evt.stopPropagation() | |
65 try: | 159 try: |
66 fullscreen_elt = document.fullscreenElement | 160 fullscreen_elt = document.fullscreenElement |
67 request_fullscreen = self.video_player_elt.requestFullscreen | 161 request_fullscreen = self.media_player_elt.requestFullscreen |
68 except AttributeError: | 162 except AttributeError: |
69 print("fullscreen is not available on this browser") | 163 print("fullscreen is not available on this browser") |
70 else: | 164 else: |
71 if fullscreen_elt == None: | 165 if fullscreen_elt == None: |
72 print("requesting fullscreen") | 166 print("requesting fullscreen") |
77 document.exitFullscreen() | 171 document.exitFullscreen() |
78 except AttributeError: | 172 except AttributeError: |
79 print("exitFullscreen not available on this browser") | 173 print("exitFullscreen not available on this browser") |
80 | 174 |
81 def on_progress_click(self, evt): | 175 def on_progress_click(self, evt): |
176 evt.stopPropagation() | |
82 position = evt.offsetX / evt.target.width | 177 position = evt.offsetX / evt.target.width |
83 new_time = self.player.duration * position | 178 new_time = self.player.duration * position |
84 self.player.currentTime = new_time | 179 self.player.currentTime = new_time |
85 | 180 |
86 def on_play(self, evt): | 181 def on_play(self, evt): |
87 self.video_player_elt.classList.add("playing") | 182 self.media_player_elt.classList.add("playing") |
88 self.show_controls() | 183 self.show_controls() |
89 self.video_player_elt.bind("mousemove", self.on_mouse_move) | 184 self.media_player_elt.bind("mousemove", self.on_mouse_move) |
90 | 185 |
91 def on_pause(self, evt): | 186 def on_pause(self, evt): |
92 self.video_player_elt.classList.remove("playing") | 187 self.media_player_elt.classList.remove("playing") |
93 self.show_controls() | 188 self.show_controls() |
94 self.video_player_elt.unbind("mousemove") | 189 self.media_player_elt.unbind("mousemove") |
95 | 190 |
96 def on_timeupdate(self, evt): | 191 def on_timeupdate(self, evt): |
97 self.update_progress() | 192 self.update_progress() |
98 | 193 |
99 def on_ended(self, evt): | 194 def on_ended(self, evt): |
100 self.update_progress() | 195 self.update_progress() |
101 | 196 |
102 def on_volumechange(self, evt): | 197 def on_volumechange(self, evt): |
198 evt.stopPropagation() | |
103 if self.player.muted: | 199 if self.player.muted: |
104 self.video_player_elt.classList.add("muted") | 200 self.media_player_elt.classList.add("muted") |
105 else: | 201 else: |
106 self.video_player_elt.classList.remove("muted") | 202 self.media_player_elt.classList.remove("muted") |
107 | 203 |
108 def on_mouse_move(self, evt): | 204 def on_mouse_move(self, evt): |
109 self.show_controls() | 205 self.show_controls() |
110 | 206 |
111 def update_progress(self): | 207 def update_progress(self): |
125 else: | 221 else: |
126 print(f"ERROR: unknown timer mode: {self.timer_mode}") | 222 print(f"ERROR: unknown timer mode: {self.timer_mode}") |
127 | 223 |
128 def hide_controls(self): | 224 def hide_controls(self): |
129 self.controls_elt.classList.add("hidden") | 225 self.controls_elt.classList.add("hidden") |
130 self.video_player_elt.style.cursor = "none" | 226 self.media_player_elt.style.cursor = "none" |
131 if self.hide_controls_timer is not None: | 227 if self.hide_controls_timer is not None: |
132 timer.clear_timeout(self.hide_controls_timer) | 228 timer.clear_timeout(self.hide_controls_timer) |
133 self.hide_controls_timer = None | 229 self.hide_controls_timer = None |
134 | 230 |
135 def show_controls(self): | 231 def show_controls(self): |
136 self.controls_elt.classList.remove("hidden") | 232 self.controls_elt.classList.remove("hidden") |
137 self.video_player_elt.style.cursor = "" | 233 self.media_player_elt.style.cursor = "" |
138 if self.hide_controls_timer is not None: | 234 if self.hide_controls_timer is not None: |
139 timer.clear_timeout(self.hide_controls_timer) | 235 timer.clear_timeout(self.hide_controls_timer) |
140 if self.player.paused: | 236 if self.player.paused: |
141 self.hide_controls_timer = None | 237 self.hide_controls_timer = None |
142 else: | 238 else: |
143 self.hide_controls_timer = timer.set_timeout(self.hide_controls, 3000) | 239 self.hide_controls_timer = timer.set_timeout(self.hide_controls, 3000) |
144 | 240 |
145 @classmethod | 241 @classmethod |
146 def do_imports(cls): | 242 def do_imports(cls): |
147 # we do imports (notably for ogv.js) if they are necessary | 243 # we do imports (notably for ogv.js) only if they are necessary |
148 if cls.imports_done: | 244 if cls.imports_done: |
149 return | 245 return |
150 from js_modules import ogv | 246 if not cls.native: |
151 cls.ogv = ogv | 247 from js_modules import ogv |
152 if not ogv.OGVCompat.supported('OGVPlayer'): | 248 cls.ogv = ogv |
153 print("Can't use OGVPlayer with this browser") | 249 if not ogv.OGVCompat.supported('OGVPlayer'): |
154 raise NotImplementedError | 250 print("Can't use OGVPlayer with this browser") |
251 raise NotImplementedError | |
155 import template | 252 import template |
156 global video_player_tpl | 253 global media_player_tpl |
157 video_player_tpl = template.Template("components/video_player.html") | 254 media_player_tpl = template.Template("components/media_player.html") |
158 cls.imports_done = True | 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 | |
159 | 264 |
160 @classmethod | 265 @classmethod |
161 def install(cls, cant_play): | 266 def install(cls, cant_play): |
267 cls.native = False | |
162 ext_list = set() | 268 ext_list = set() |
163 for data in cant_play.values(): | 269 for data in cant_play.values(): |
164 ext_list.update(data['ext']) | 270 ext_list.update(data['ext']) |
165 for ori_video_elt in document.body.select('video'): | 271 cls.cant_play_ext_list = ext_list |
272 for to_rpl_vid_elt in document.body.select('video'): | |
166 sources = [] | 273 sources = [] |
167 src = (ori_video_elt.src or '').strip() | 274 src = (to_rpl_vid_elt.src or '').strip() |
168 if src: | 275 if src: |
169 sources.append(src) | 276 sources.append(src) |
170 | 277 |
171 for source_elt in ori_video_elt.select('source'): | 278 for source_elt in to_rpl_vid_elt.select('source'): |
172 src = (source_elt.src or '').strip() | 279 src = (source_elt.src or '').strip() |
173 if src: | 280 if src: |
174 sources.append(src) | 281 sources.append(src) |
175 | 282 |
176 # FIXME: we only use first found source | 283 # FIXME: we only use first found source |
177 try: | 284 try: |
178 source = sources[0] | 285 source = sources[0] |
179 except IndexError: | 286 except IndexError: |
180 print(f"Can't find any source for following elt:\n{ori_video_elt.html}") | 287 print(f"Can't find any source for following elt:\n{to_rpl_vid_elt.html}") |
181 continue | 288 continue |
182 | 289 |
183 try: | 290 ext = cls.get_source_ext(source) |
184 ext = f".{source.rsplit('.', 1)[1]}" | 291 |
185 except IndexError: | 292 ext = f".{source.rsplit('.', 1)[1]}" |
293 if ext is None: | |
186 print( | 294 print( |
187 f"No extension found for source of following elt:\n{ori_video_elt.html}") | 295 "No extension found for source of following elt:\n" |
296 f"{to_rpl_vid_elt.html}" | |
297 ) | |
188 continue | 298 continue |
189 if ext and ext in ext_list: | 299 if ext in ext_list: |
190 cls.do_imports() | |
191 print(f"alternative player will be used for {source!r}") | 300 print(f"alternative player will be used for {source!r}") |
192 cls(ori_video_elt, sources) | 301 cls(sources, to_rpl_vid_elt) |
193 | 302 |
194 | 303 |
195 def install_if_needed(): | 304 def install_if_needed(): |
196 CONTENT_TYPES = { | 305 CONTENT_TYPES = { |
197 "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogg"]}, | 306 "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogv", ".ogg"]}, |
198 "webm_vp8": {"type": 'video/webm; codecs="vp8, vorbis"', "ext": [".webm"]}, | 307 "webm_vp8": {"type": 'video/webm; codecs="vp8, vorbis"', "ext": [".webm"]}, |
199 "webm_vp9": {"type": 'video/webm; codecs="vp9"', "ext": [".webm"]}, | 308 "webm_vp9": {"type": 'video/webm; codecs="vp9"', "ext": [".webm"]}, |
200 # FIXME: handle audio | 309 # FIXME: handle audio |
201 # "ogg_vorbis": {"type": 'audio/ogg; codecs="vorbis"', "ext": ".ogg"}, | 310 # "ogg_vorbis": {"type": 'audio/ogg; codecs="vorbis"', "ext": ".ogg"}, |
202 } | 311 } |
203 test_video_elt = document.createElement("video") | 312 test_media_elt = html.VIDEO() |
204 cant_play = {k:d for k,d in CONTENT_TYPES.items() if test_video_elt.canPlayType(d['type']) != "probably"} | 313 cant_play = {k:d for k,d in CONTENT_TYPES.items() |
314 if test_media_elt.canPlayType(d['type']) != "probably"} | |
205 | 315 |
206 if cant_play: | 316 if cant_play: |
207 cant_play_list = '\n'.join(f"- {k} ({d['type']})" for k, d in cant_play.items()) | 317 cant_play_list = '\n'.join(f"- {k} ({d['type']})" for k, d in cant_play.items()) |
208 print( | 318 print( |
209 "This browser is incompatible with following content types, using alternative:\n" | 319 "This browser is incompatible with following content types, using " |
210 f"{cant_play_list}" | 320 f"alternative:\n{cant_play_list}" |
211 ) | 321 ) |
212 try: | 322 try: |
213 VideoPlayer.install(cant_play) | 323 MediaPlayer.install(cant_play) |
214 except NotImplementedError: | 324 except NotImplementedError: |
215 pass | 325 pass |
216 else: | 326 else: |
217 print("This browser can play natively all requested open video/audio formats") | 327 print("This browser can play natively all requested open video/audio formats") |