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