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")