Mercurial > libervia-web
comparison libervia/web/pages/calls/_browser/__init__.py @ 1553:83c2a6faa2ae
browser (calls): screen sharing implementation:
- the new screen sharing button toggle screen sharing state
- the button reflect the screen sharing state (green crossed when not sharing, red
uncrossed otherwise)
- the screen sharing stream replaces the camera one, and vice versa. No re-negociation is
needed.
- stopping the sharing through browser's dialog is supported
- the screen sharing button is only visibile if supported by the platform
rel 432
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 14 Aug 2023 16:49:02 +0200 |
parents | e47c24204449 |
children | 855729ef75f2 |
comparison
equal
deleted
inserted
replaced
1552:c62027660ec1 | 1553:83c2a6faa2ae |
---|---|
13 log.warning = log.warn | 13 log.warning = log.warn |
14 profile = window.profile or "" | 14 profile = window.profile or "" |
15 bridge = Bridge() | 15 bridge = Bridge() |
16 GATHER_TIMEOUT = 10000 | 16 GATHER_TIMEOUT = 10000 |
17 ALLOWED_STATUSES = (None, "dialing") | 17 ALLOWED_STATUSES = (None, "dialing") |
18 AUDIO = 'audio' | 18 AUDIO = "audio" |
19 VIDEO = 'video' | 19 VIDEO = "video" |
20 ALLOWED_CALL_MODES = {AUDIO, VIDEO} | 20 ALLOWED_CALL_MODES = {AUDIO, VIDEO} |
21 INACTIVE_CLASS = "inactive" | |
22 MUTED_CLASS = "muted" | |
23 SCREEN_OFF_CLASS = "screen-off" | |
21 | 24 |
22 | 25 |
23 class CallUI: | 26 class CallUI: |
24 | |
25 def __init__(self): | 27 def __init__(self): |
26 self.webrtc = WebRTC() | 28 self.webrtc = WebRTC() |
29 self.webrtc.screen_sharing_cb = self.on_sharing_screen | |
27 self.mode = "search" | 30 self.mode = "search" |
28 self._status = None | 31 self._status = None |
29 self._callee = None | 32 self._callee = None |
30 self.contacts_elt = document["contacts"] | 33 self.contacts_elt = document["contacts"] |
31 self.search_container_elt = document["search_container"] | 34 self.search_container_elt = document["search_container"] |
42 | 45 |
43 # call/hang up buttons | 46 # call/hang up buttons |
44 self._call_mode = VIDEO | 47 self._call_mode = VIDEO |
45 self.call_button_tpl = Template("call/call_button.html") | 48 self.call_button_tpl = Template("call/call_button.html") |
46 self._update_call_button() | 49 self._update_call_button() |
47 document['toggle_call_mode_btn'].bind('click', self.switch_call_mode) | 50 document["toggle_call_mode_btn"].bind("click", self.switch_call_mode) |
48 document["hangup_btn"].bind( | 51 document["hangup_btn"].bind("click", lambda __: aio.run(self.hang_up())) |
49 "click", | 52 |
50 lambda __: aio.run(self.hang_up()) | 53 # other buttons |
54 document["full_screen_btn"].bind("click", lambda __: self.toggle_fullscreen()) | |
55 document["exit_full_screen_btn"].bind( | |
56 "click", lambda __: self.toggle_fullscreen() | |
51 ) | 57 ) |
52 | 58 document["mute_audio_btn"].bind("click", self.toggle_audio_mute) |
53 # other buttons | 59 document["mute_video_btn"].bind("click", self.toggle_video_mute) |
54 document["full_screen_btn"].bind( | 60 self.share_desktop_btn_elt = document["share_desktop_btn"] |
55 "click", | 61 if hasattr(window.navigator.mediaDevices, "getDisplayMedia"): |
56 lambda __: self.toggle_fullscreen() | 62 self.share_desktop_btn_elt.classList.remove("is-hidden-touch") |
57 ) | 63 # screen sharing is supported |
58 document["exit_full_screen_btn"].bind( | 64 self.share_desktop_btn_elt.bind("click", self.toggle_screen_sharing) |
59 "click", | 65 else: |
60 lambda __: self.toggle_fullscreen() | 66 self.share_desktop_btn_elt.classList.add("is-hidden") |
61 ) | |
62 document["mute_audio_btn"].bind( | |
63 "click", | |
64 self.toggle_audio_mute | |
65 ) | |
66 document["mute_video_btn"].bind( | |
67 "click", | |
68 self.toggle_video_mute | |
69 ) | |
70 | 67 |
71 # search | 68 # search |
72 self.jid_search = JidSearch( | 69 self.jid_search = JidSearch( |
73 document["search"], | 70 document["search"], document["contacts"], click_cb=self._on_entity_click |
74 document["contacts"], | |
75 click_cb = self._on_entity_click | |
76 ) | 71 ) |
77 | 72 |
78 # incoming call dialog | 73 # incoming call dialog |
79 self.incoming_call_dialog_elt = None | 74 self.incoming_call_dialog_elt = None |
80 | 75 |
81 @property | 76 @property |
82 def sid(self) -> str|None: | 77 def sid(self) -> str | None: |
83 return self.webrtc.sid | 78 return self.webrtc.sid |
84 | 79 |
85 @sid.setter | 80 @sid.setter |
86 def sid(self, new_sid) -> None: | 81 def sid(self, new_sid) -> None: |
87 self.webrtc.sid = new_sid | 82 self.webrtc.sid = new_sid |
95 if new_status != self._status: | 90 if new_status != self._status: |
96 if new_status not in ALLOWED_STATUSES: | 91 if new_status not in ALLOWED_STATUSES: |
97 raise Exception( | 92 raise Exception( |
98 f"INTERNAL ERROR: this status is not allowed: {new_status!r}" | 93 f"INTERNAL ERROR: this status is not allowed: {new_status!r}" |
99 ) | 94 ) |
100 tpl_data = { | 95 tpl_data = {"entity": self._callee, "status": new_status} |
101 "entity": self._callee, | |
102 "status": new_status | |
103 } | |
104 if self._callee is not None: | 96 if self._callee is not None: |
105 try: | 97 try: |
106 tpl_data["name"] = cache.identities[self._callee]["nicknames"][0] | 98 tpl_data["name"] = cache.identities[self._callee]["nicknames"][0] |
107 except (KeyError, IndexError): | 99 except (KeyError, IndexError): |
108 tpl_data["name"] = str(self._callee) | 100 tpl_data["name"] = str(self._callee) |
109 status_elt = self.call_status_tpl.get_elt(tpl_data) | 101 status_elt = self.call_status_tpl.get_elt(tpl_data) |
110 self.call_status_wrapper_elt.clear() | 102 self.call_status_wrapper_elt.clear() |
111 self.call_status_wrapper_elt <= status_elt | 103 self.call_status_wrapper_elt <= status_elt |
112 | |
113 | 104 |
114 self._status = new_status | 105 self._status = new_status |
115 | 106 |
116 @property | 107 @property |
117 def call_mode(self): | 108 def call_mode(self): |
137 self.call_mode = AUDIO if self.call_mode == VIDEO else VIDEO | 128 self.call_mode = AUDIO if self.call_mode == VIDEO else VIDEO |
138 | 129 |
139 def _update_call_button(self): | 130 def _update_call_button(self): |
140 new_button = self.call_button_tpl.get_elt({"call_mode": self.call_mode}) | 131 new_button = self.call_button_tpl.get_elt({"call_mode": self.call_mode}) |
141 new_button.bind( | 132 new_button.bind( |
142 "click", | 133 "click", lambda __: aio.run(self.make_call(video=not self.call_mode == AUDIO)) |
143 lambda __: aio.run(self.make_call(video=not self.call_mode == AUDIO)) | |
144 ) | 134 ) |
145 document['call_btn'].replaceWith(new_button) | 135 document["call_btn"].replaceWith(new_button) |
146 | 136 |
147 def _on_action_new( | 137 def _on_action_new( |
148 self, action_data_s: str, action_id: str, security_limit: int, profile: str | 138 self, action_data_s: str, action_id: str, security_limit: int, profile: str |
149 ) -> None: | 139 ) -> None: |
150 """Called when a call is received | 140 """Called when a call is received |
159 return | 149 return |
160 aio.run(self.on_action_new(action_data, action_id)) | 150 aio.run(self.on_action_new(action_data, action_id)) |
161 | 151 |
162 async def on_action_new(self, action_data: dict, action_id: str) -> None: | 152 async def on_action_new(self, action_data: dict, action_id: str) -> None: |
163 peer_jid = action_data["from_jid"] | 153 peer_jid = action_data["from_jid"] |
164 log.info( | 154 log.info(f"{peer_jid} wants to start a call ({action_data['sub_type']})") |
165 f"{peer_jid} wants to start a call ({action_data['sub_type']})" | |
166 ) | |
167 if self.sid is not None: | 155 if self.sid is not None: |
168 log.warning( | 156 log.warning( |
169 f"already in a call ({self.sid}), can't receive a new call from " | 157 f"already in a call ({self.sid}), can't receive a new call from " |
170 f"{peer_jid}" | 158 f"{peer_jid}" |
171 ) | 159 ) |
172 return | 160 return |
173 sid = self.sid = action_data["session_id"] | 161 sid = self.sid = action_data["session_id"] |
174 await cache.fill_identities([peer_jid]) | 162 await cache.fill_identities([peer_jid]) |
175 identity = cache.identities[peer_jid] | 163 identity = cache.identities[peer_jid] |
176 peer_name = identity['nicknames'][0] | 164 peer_name = identity["nicknames"][0] |
177 | 165 |
178 # we start the ring | 166 # we start the ring |
179 self.audio_player_elt.play() | 167 self.audio_player_elt.play() |
180 | 168 |
181 # and ask user if we take the call | 169 # and ask user if we take the call |
182 try: | 170 try: |
183 self.incoming_call_dialog_elt = dialog.Confirm( | 171 self.incoming_call_dialog_elt = dialog.Confirm( |
184 f"{peer_name} is calling you.", | 172 f"{peer_name} is calling you.", ok_label="Answer", cancel_label="Reject" |
185 ok_label="Answer", | |
186 cancel_label="Reject" | |
187 ) | 173 ) |
188 accepted = await self.incoming_call_dialog_elt.ashow() | 174 accepted = await self.incoming_call_dialog_elt.ashow() |
189 except dialog.CancelError: | 175 except dialog.CancelError: |
190 log.info("Call has been cancelled") | 176 log.info("Call has been cancelled") |
191 self.incoming_call_dialog_elt = None | 177 self.incoming_call_dialog_elt = None |
192 self.sid = None | 178 self.sid = None |
193 dialog.notification.show( | 179 dialog.notification.show(f"{peer_name} has cancelled the call", level="info") |
194 f"{peer_name} has cancelled the call", | |
195 level="info" | |
196 ) | |
197 return | 180 return |
198 | 181 |
199 self.incoming_call_dialog_elt = None | 182 self.incoming_call_dialog_elt = None |
200 | 183 |
201 # we stop the ring | 184 # we stop the ring |
208 # Answer the call | 191 # Answer the call |
209 self.switch_mode("call") | 192 self.switch_mode("call") |
210 else: | 193 else: |
211 log.info(f"your are declining the call from {peer_jid}") | 194 log.info(f"your are declining the call from {peer_jid}") |
212 self.sid = None | 195 self.sid = None |
213 await bridge.action_launch( | 196 await bridge.action_launch(action_id, json.dumps({"cancelled": not accepted})) |
214 action_id, | |
215 json.dumps({"cancelled": not accepted}) | |
216 ) | |
217 | 197 |
218 def _on_call_ended(self, session_id: str, data_s: str, profile: str) -> None: | 198 def _on_call_ended(self, session_id: str, data_s: str, profile: str) -> None: |
219 """Call has been terminated | 199 """Call has been terminated |
220 | 200 |
221 @param session_id: Session identifier | 201 @param session_id: Session identifier |
239 @param sdp: Session Description Protocol data | 219 @param sdp: Session Description Protocol data |
240 @param profile: Profile associated with the action | 220 @param profile: Profile associated with the action |
241 """ | 221 """ |
242 aio.run(self.on_call_setup(session_id, json.loads(setup_data_s), profile)) | 222 aio.run(self.on_call_setup(session_id, json.loads(setup_data_s), profile)) |
243 | 223 |
244 async def on_call_setup(self, session_id: str, setup_data: dict, profile: str) -> None: | 224 async def on_call_setup( |
225 self, session_id: str, setup_data: dict, profile: str | |
226 ) -> None: | |
245 """Call has been accepted, connection can be established | 227 """Call has been accepted, connection can be established |
246 | 228 |
247 @param session_id: Session identifier | 229 @param session_id: Session identifier |
248 @param setup_data: Data with following keys: | 230 @param setup_data: Data with following keys: |
249 role: initiator or responser | 231 role: initiator or responser |
258 try: | 240 try: |
259 role = setup_data["role"] | 241 role = setup_data["role"] |
260 sdp = setup_data["sdp"] | 242 sdp = setup_data["sdp"] |
261 except KeyError: | 243 except KeyError: |
262 dialog.notification.show( | 244 dialog.notification.show( |
263 f"Invalid setup data received: {setup_data}", | 245 f"Invalid setup data received: {setup_data}", level="error" |
264 level="error" | |
265 ) | 246 ) |
266 return | 247 return |
267 if role == "initiator": | 248 if role == "initiator": |
268 await self.webrtc.accept_call(session_id, sdp, profile) | 249 await self.webrtc.accept_call(session_id, sdp, profile) |
269 elif role == "responder": | 250 elif role == "responder": |
270 await self.webrtc.answer_call(session_id, sdp, profile) | 251 await self.webrtc.answer_call(session_id, sdp, profile) |
271 else: | 252 else: |
272 dialog.notification.show( | 253 dialog.notification.show( |
273 f"Invalid role received during setup: {setup_data}", | 254 f"Invalid role received during setup: {setup_data}", level="error" |
274 level="error" | |
275 ) | 255 ) |
276 return | 256 return |
277 | 257 |
278 async def make_call(self, audio: bool = True, video: bool = True) -> None: | 258 async def make_call(self, audio: bool = True, video: bool = True) -> None: |
279 """Start a WebRTC call | 259 """Start a WebRTC call |
285 callee_jid = JID(document["search"].value.strip()) | 265 callee_jid = JID(document["search"].value.strip()) |
286 if not callee_jid.is_valid: | 266 if not callee_jid.is_valid: |
287 raise ValueError | 267 raise ValueError |
288 except ValueError: | 268 except ValueError: |
289 dialog.notification.show( | 269 dialog.notification.show( |
290 "Invalid identifier, please use a valid callee identifier", | 270 "Invalid identifier, please use a valid callee identifier", level="error" |
291 level="error" | |
292 ) | 271 ) |
293 return | 272 return |
294 | 273 |
295 self._callee = callee_jid | 274 self._callee = callee_jid |
296 await cache.fill_identities([callee_jid]) | 275 await cache.fill_identities([callee_jid]) |
297 self.status = "dialing" | 276 self.status = "dialing" |
298 call_avatar_elt = self.call_avatar_tpl.get_elt({ | 277 call_avatar_elt = self.call_avatar_tpl.get_elt( |
299 "entity": str(callee_jid), | 278 { |
300 "identities": cache.identities, | 279 "entity": str(callee_jid), |
301 }) | 280 "identities": cache.identities, |
281 } | |
282 ) | |
302 self.call_avatar_wrapper_elt.clear() | 283 self.call_avatar_wrapper_elt.clear() |
303 self.call_avatar_wrapper_elt <= call_avatar_elt | 284 self.call_avatar_wrapper_elt <= call_avatar_elt |
304 | |
305 | 285 |
306 self.switch_mode("call") | 286 self.switch_mode("call") |
307 await self.webrtc.make_call(callee_jid, audio, video) | 287 await self.webrtc.make_call(callee_jid, audio, video) |
308 | 288 |
309 async def end_call(self, data: dict) -> None: | 289 async def end_call(self, data: dict) -> None: |
333 session_id = self.sid | 313 session_id = self.sid |
334 if not session_id: | 314 if not session_id: |
335 log.warning("Can't hand_up, not call in progress") | 315 log.warning("Can't hand_up, not call in progress") |
336 return | 316 return |
337 await self.end_call({"reason": "terminated"}) | 317 await self.end_call({"reason": "terminated"}) |
338 await bridge.call_end( | 318 await bridge.call_end(session_id, "") |
339 session_id, | |
340 "" | |
341 ) | |
342 | 319 |
343 def _handle_animation_end( | 320 def _handle_animation_end( |
344 self, | 321 self, |
345 element, | 322 element, |
346 remove = None, | 323 remove=None, |
347 add = None, | 324 add=None, |
348 ): | 325 ): |
349 """Return a handler that removes specified classes and the event handler. | 326 """Return a handler that removes specified classes and the event handler. |
350 | 327 |
351 @param element: The element to operate on. | 328 @param element: The element to operate on. |
352 @param remove: List of class names to remove from the element. | 329 @param remove: List of class names to remove from the element. |
353 @param add: List of class names to add to the element. | 330 @param add: List of class names to add to the element. |
354 """ | 331 """ |
332 | |
355 def handler(__, remove=remove, add=add): | 333 def handler(__, remove=remove, add=add): |
356 log.info(f"animation end OK {element=}") | 334 log.info(f"animation end OK {element=}") |
357 if add: | 335 if add: |
358 if isinstance(add, str): | 336 if isinstance(add, str): |
359 add = [add] | 337 add = [add] |
360 element.classList.add(*add) | 338 element.classList.add(*add) |
361 if remove: | 339 if remove: |
362 if isinstance(remove, str): | 340 if isinstance(remove, str): |
363 remove = [remove] | 341 remove = [remove] |
364 element.classList.remove(*remove) | 342 element.classList.remove(*remove) |
365 element.unbind('animationend', handler) | 343 element.unbind("animationend", handler) |
366 | 344 |
367 return handler | 345 return handler |
368 | 346 |
369 def switch_mode(self, mode: str) -> None: | 347 def switch_mode(self, mode: str) -> None: |
370 """Handles the user interface changes""" | 348 """Handles the user interface changes""" |
372 return | 350 return |
373 if mode == "call": | 351 if mode == "call": |
374 # Hide contacts with fade-out animation and bring up the call box | 352 # Hide contacts with fade-out animation and bring up the call box |
375 self.search_container_elt.classList.add("fade-out-y") | 353 self.search_container_elt.classList.add("fade-out-y") |
376 self.search_container_elt.bind( | 354 self.search_container_elt.bind( |
377 'animationend', | 355 "animationend", |
378 self._handle_animation_end( | 356 self._handle_animation_end( |
379 self.search_container_elt, | 357 self.search_container_elt, remove="fade-out-y", add="is-hidden" |
380 remove="fade-out-y", | 358 ), |
381 add="is-hidden" | |
382 ) | |
383 ) | 359 ) |
384 self.call_container_elt.classList.remove("is-hidden") | 360 self.call_container_elt.classList.remove("is-hidden") |
385 self.call_container_elt.classList.add("slide-in") | 361 self.call_container_elt.classList.add("slide-in") |
386 self.call_container_elt.bind( | 362 self.call_container_elt.bind( |
387 'animationend', | 363 "animationend", |
388 self._handle_animation_end( | 364 self._handle_animation_end(self.call_container_elt, remove="slide-in"), |
389 self.call_container_elt, | |
390 remove="slide-in" | |
391 ) | |
392 ) | 365 ) |
393 self.mode = mode | 366 self.mode = mode |
394 elif mode == "search": | 367 elif mode == "search": |
395 self.toggle_fullscreen(False) | 368 self.toggle_fullscreen(False) |
396 self.search_container_elt.classList.add("fade-out-y", "animation-reverse") | 369 self.search_container_elt.classList.add("fade-out-y", "animation-reverse") |
397 self.search_container_elt.classList.remove("is-hidden") | 370 self.search_container_elt.classList.remove("is-hidden") |
398 self.search_container_elt.bind( | 371 self.search_container_elt.bind( |
399 'animationend', | 372 "animationend", |
400 self._handle_animation_end( | 373 self._handle_animation_end( |
401 self.search_container_elt, | 374 self.search_container_elt, |
402 remove=["fade-out-y", "animation-reverse"], | 375 remove=["fade-out-y", "animation-reverse"], |
403 ) | 376 ), |
404 ) | 377 ) |
405 self.call_container_elt.classList.add("slide-in", "animation-reverse") | 378 self.call_container_elt.classList.add("slide-in", "animation-reverse") |
406 self.call_container_elt.bind( | 379 self.call_container_elt.bind( |
407 'animationend', | 380 "animationend", |
408 self._handle_animation_end( | 381 self._handle_animation_end( |
409 self.call_container_elt, | 382 self.call_container_elt, |
410 remove=["slide-in", "animation-reverse"], | 383 remove=["slide-in", "animation-reverse"], |
411 add="is-hidden" | 384 add="is-hidden", |
412 ) | 385 ), |
413 ) | 386 ) |
414 self.mode = mode | 387 self.mode = mode |
415 else: | 388 else: |
416 log.error(f"Internal Error: Unknown call mode: {mode}") | 389 log.error(f"Internal Error: Unknown call mode: {mode}") |
417 | 390 |
418 def toggle_fullscreen(self, fullscreen: bool|None = None): | 391 def toggle_fullscreen(self, fullscreen: bool | None = None): |
419 """Toggle fullscreen mode for video elements. | 392 """Toggle fullscreen mode for video elements. |
420 | 393 |
421 @param fullscreen: if set, determine the fullscreen state; otherwise, | 394 @param fullscreen: if set, determine the fullscreen state; otherwise, |
422 the fullscreen mode will be toggled. | 395 the fullscreen mode will be toggled. |
423 """ | 396 """ |
437 document["full_screen_btn"].classList.remove("is-hidden") | 410 document["full_screen_btn"].classList.remove("is-hidden") |
438 document["exit_full_screen_btn"].classList.add("is-hidden") | 411 document["exit_full_screen_btn"].classList.add("is-hidden") |
439 | 412 |
440 except Exception as e: | 413 except Exception as e: |
441 dialog.notification.show( | 414 dialog.notification.show( |
442 f"An error occurred while toggling fullscreen: {e}", | 415 f"An error occurred while toggling fullscreen: {e}", level="error" |
443 level="error" | |
444 ) | 416 ) |
445 | 417 |
446 def toggle_audio_mute(self, evt): | 418 def toggle_audio_mute(self, evt): |
447 is_muted = self.webrtc.toggle_audio_mute() | 419 is_muted = self.webrtc.toggle_audio_mute() |
448 btn_elt = evt.currentTarget | 420 btn_elt = evt.currentTarget |
449 if is_muted: | 421 if is_muted: |
450 btn_elt.classList.remove("is-success") | 422 btn_elt.classList.remove("is-success") |
451 btn_elt.classList.add("muted", "is-warning") | 423 btn_elt.classList.add(INACTIVE_CLASS, MUTED_CLASS, "is-warning") |
452 dialog.notification.show( | 424 dialog.notification.show( |
453 f"audio is now muted", | 425 f"audio is now muted", |
454 level="info", | 426 level="info", |
455 delay=2, | 427 delay=2, |
456 ) | 428 ) |
457 else: | 429 else: |
458 btn_elt.classList.remove("muted", "is-warning") | 430 btn_elt.classList.remove(INACTIVE_CLASS, MUTED_CLASS, "is-warning") |
459 btn_elt.classList.add("is-success") | 431 btn_elt.classList.add("is-success") |
460 | 432 |
461 def toggle_video_mute(self, evt): | 433 def toggle_video_mute(self, evt): |
462 is_muted = self.webrtc.toggle_video_mute() | 434 is_muted = self.webrtc.toggle_video_mute() |
463 btn_elt = evt.currentTarget | 435 btn_elt = evt.currentTarget |
464 if is_muted: | 436 if is_muted: |
465 btn_elt.classList.remove("is-success") | 437 btn_elt.classList.remove("is-success") |
466 btn_elt.classList.add("muted", "is-warning") | 438 btn_elt.classList.add(INACTIVE_CLASS, MUTED_CLASS, "is-warning") |
467 dialog.notification.show( | 439 dialog.notification.show( |
468 f"video is now muted", | 440 f"video is now muted", |
469 level="info", | 441 level="info", |
470 delay=2, | 442 delay=2, |
471 ) | 443 ) |
472 else: | 444 else: |
473 btn_elt.classList.remove("muted", "is-warning") | 445 btn_elt.classList.remove(INACTIVE_CLASS, MUTED_CLASS, "is-warning") |
474 btn_elt.classList.add("is-success") | 446 btn_elt.classList.add("is-success") |
447 | |
448 def toggle_screen_sharing(self, evt): | |
449 aio.run(self.webrtc.toggle_screen_sharing()) | |
450 | |
451 def on_sharing_screen(self, sharing: bool) -> None: | |
452 """Called when screen sharing state changes""" | |
453 share_desktop_btn_elt = self.share_desktop_btn_elt | |
454 if sharing: | |
455 share_desktop_btn_elt.classList.add("is-danger") | |
456 share_desktop_btn_elt.classList.remove(INACTIVE_CLASS, SCREEN_OFF_CLASS) | |
457 else: | |
458 share_desktop_btn_elt.classList.remove("is-danger") | |
459 share_desktop_btn_elt.classList.add(INACTIVE_CLASS, SCREEN_OFF_CLASS) | |
475 | 460 |
476 def _on_entity_click(self, item: dict) -> None: | 461 def _on_entity_click(self, item: dict) -> None: |
477 aio.run(self.on_entity_click(item)) | 462 aio.run(self.on_entity_click(item)) |
478 | 463 |
479 async def on_entity_click(self, item: dict) -> None: | 464 async def on_entity_click(self, item: dict) -> None: |