comparison libervia/pages/_browser/alt_media_player.py @ 1337:f0648005cd11

browser: alternative media player: the new `alt_media_player` module implements an `ogv.js` based media player which is able to play WebM (VP8/VP9) or Ogg (Theora, Vorbis) formats (+ other codecs not integrated yet) on browser that don't do it natively. The module imports `ogv.js` only when it's necessary (incompatible browser + videos needing it). This for now used in blog, and will be used in other places when it's suitable.
author Goffi <goffi@goffi.org>
date Sat, 15 Aug 2020 16:45:23 +0200
parents
children 472267dcd4d8
comparison
equal deleted inserted replaced
1336:71f24ad99a2c 1337:f0648005cd11
1 #!/usr/bin/env python3
2
3 """This module implement an ogv.js based alternative media player
4
5 This is useful to play libre video/audio formats on browser that don't do it natively.
6 """
7
8 from browser import document, timer
9
10
11 class VideoPlayer:
12 TIMER_MODES = ("timer", "remaining")
13 imports_done = False
14
15 def __init__(self, ori_video_elt, sources):
16 self.video_player_elt = video_player_elt = video_player_tpl.get_elt()
17 self.player = player = self.ogv.OGVPlayer.new() # {"debug": True})
18 ori_video_elt.parentNode.replaceChild(video_player_elt, ori_video_elt)
19 overlay_play_elt = self.video_player_elt.select_one(".video_overlay_play")
20 overlay_play_elt.bind("click", self.on_overlay_play_elt_click)
21 self.progress_elt = video_player_elt.select_one("progress")
22 self.progress_elt.bind("click", self.on_progress_click)
23 self.timer_elt = video_player_elt.select_one(".timer")
24 self.timer_mode = "timer"
25 self.controls_elt = video_player_elt.select_one(".video_controls")
26 player_wrapper_elt = video_player_elt.select_one(".video_elt")
27 player.src = sources[0]
28 player_wrapper_elt <= player
29 self.hide_controls_timer = None
30
31 # a click on the video itself is like click on play icon
32 player_wrapper_elt.bind("click", self.on_play_click)
33
34 # buttons
35 for handler in ("play", "change_timer_mode", "change_volume", "fullscreen"):
36 for elt in video_player_elt.select(f".click_to_{handler}"):
37 elt.bind("click", getattr(self, f"on_{handler}_click"))
38 # events
39 # FIXME: progress is not implemented in OGV.js, update when available
40 for event in ("play", "pause", "timeupdate", "ended", "volumechange"):
41 player.bind(event, getattr(self, f"on_{event}"))
42
43 def on_overlay_play_elt_click(self, evt):
44 evt.stopPropagation()
45 evt.target.remove()
46 self.player.play()
47
48 def on_play_click(self, evt):
49 if self.player.paused:
50 print("playing")
51 self.player.play()
52 else:
53 self.player.pause()
54 print("paused")
55
56 def on_change_timer_mode_click(self, evt):
57 self.timer_mode = self.TIMER_MODES[
58 (self.TIMER_MODES.index(self.timer_mode) + 1) % len(self.TIMER_MODES)
59 ]
60
61 def on_change_volume_click(self, evt):
62 self.player.muted = not self.player.muted
63
64 def on_fullscreen_click(self, evt):
65 try:
66 fullscreen_elt = document.fullscreenElement
67 request_fullscreen = self.video_player_elt.requestFullscreen
68 except AttributeError:
69 print("fullscreen is not available on this browser")
70 else:
71 if fullscreen_elt == None:
72 print("requesting fullscreen")
73 request_fullscreen()
74 else:
75 print(f"leaving fullscreen: {fullscreen_elt}")
76 try:
77 document.exitFullscreen()
78 except AttributeError:
79 print("exitFullscreen not available on this browser")
80
81 def on_progress_click(self, evt):
82 position = evt.offsetX / evt.target.width
83 new_time = self.player.duration * position
84 self.player.currentTime = new_time
85
86 def on_play(self, evt):
87 self.video_player_elt.classList.add("playing")
88 self.show_controls()
89 self.video_player_elt.bind("mousemove", self.on_mouse_move)
90
91 def on_pause(self, evt):
92 self.video_player_elt.classList.remove("playing")
93 self.show_controls()
94 self.video_player_elt.unbind("mousemove")
95
96 def on_timeupdate(self, evt):
97 self.update_progress()
98
99 def on_ended(self, evt):
100 self.update_progress()
101
102 def on_volumechange(self, evt):
103 if self.player.muted:
104 self.video_player_elt.classList.add("muted")
105 else:
106 self.video_player_elt.classList.remove("muted")
107
108 def on_mouse_move(self, evt):
109 self.show_controls()
110
111 def update_progress(self):
112 duration = self.player.duration
113 current_time = duration if self.player.ended else self.player.currentTime
114 self.progress_elt.max = duration
115 self.progress_elt.value = current_time
116 self.progress_elt.text = f"{current_time/duration*100:.02f}"
117 current_time, duration = int(current_time), int(duration)
118 if self.timer_mode == "timer":
119 cur_min, cur_sec = divmod(current_time, 60)
120 tot_min, tot_sec = divmod(duration, 60)
121 self.timer_elt.text = f"{cur_min}:{cur_sec:02d}/{tot_min}:{tot_sec:02d}"
122 elif self.timer_mode == "remaining":
123 rem_min, rem_sec = divmod(duration - current_time, 60)
124 self.timer_elt.text = f"{rem_min}:{rem_sec:02d}"
125 else:
126 print(f"ERROR: unknown timer mode: {self.timer_mode}")
127
128 def hide_controls(self):
129 self.controls_elt.classList.add("hidden")
130 self.video_player_elt.style.cursor = "none"
131 if self.hide_controls_timer is not None:
132 timer.clear_timeout(self.hide_controls_timer)
133 self.hide_controls_timer = None
134
135 def show_controls(self):
136 self.controls_elt.classList.remove("hidden")
137 self.video_player_elt.style.cursor = ""
138 if self.hide_controls_timer is not None:
139 timer.clear_timeout(self.hide_controls_timer)
140 if self.player.paused:
141 self.hide_controls_timer = None
142 else:
143 self.hide_controls_timer = timer.set_timeout(self.hide_controls, 3000)
144
145 @classmethod
146 def do_imports(cls):
147 # we do imports (notably for ogv.js) if they are necessary
148 if cls.imports_done:
149 return
150 from js_modules import ogv
151 cls.ogv = ogv
152 if not ogv.OGVCompat.supported('OGVPlayer'):
153 print("Can't use OGVPlayer with this browser")
154 raise NotImplementedError
155 import template
156 global video_player_tpl
157 video_player_tpl = template.Template("components/video_player.html")
158 cls.imports_done = True
159
160 @classmethod
161 def install(cls, cant_play):
162 ext_list = set()
163 for data in cant_play.values():
164 ext_list.update(data['ext'])
165 for ori_video_elt in document.body.select('video'):
166 sources = []
167 src = (ori_video_elt.src or '').strip()
168 if src:
169 sources.append(src)
170
171 for source_elt in ori_video_elt.select('source'):
172 src = (source_elt.src or '').strip()
173 if src:
174 sources.append(src)
175
176 # FIXME: we only use first found source
177 try:
178 source = sources[0]
179 except IndexError:
180 print(f"Can't find any source for following elt:\n{ori_video_elt.html}")
181 continue
182
183 try:
184 ext = f".{source.rsplit('.', 1)[1]}"
185 except IndexError:
186 print(
187 f"No extension found for source of following elt:\n{ori_video_elt.html}")
188 continue
189 if ext and ext in ext_list:
190 cls.do_imports()
191 print(f"alternative player will be used for {source!r}")
192 cls(ori_video_elt, sources)
193
194
195 def install_if_needed():
196 CONTENT_TYPES = {
197 "ogg_theora": {"type": 'video/ogg; codecs="theora"', "ext": [".ogg"]},
198 "webm_vp8": {"type": 'video/webm; codecs="vp8, vorbis"', "ext": [".webm"]},
199 "webm_vp9": {"type": 'video/webm; codecs="vp9"', "ext": [".webm"]},
200 # FIXME: handle audio
201 # "ogg_vorbis": {"type": 'audio/ogg; codecs="vorbis"', "ext": ".ogg"},
202 }
203 test_video_elt = document.createElement("video")
204 cant_play = {k:d for k,d in CONTENT_TYPES.items() if test_video_elt.canPlayType(d['type']) != "probably"}
205
206 if cant_play:
207 cant_play_list = '\n'.join(f"- {k} ({d['type']})" for k, d in cant_play.items())
208 print(
209 "This browser is incompatible with following content types, using alternative:\n"
210 f"{cant_play_list}"
211 )
212 try:
213 VideoPlayer.install(cant_play)
214 except NotImplementedError:
215 pass
216 else:
217 print("This browser can play natively all requested open video/audio formats")