Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- a/libervia/web/pages/calls/_browser/__init__.py Wed Aug 09 00:48:21 2023 +0200 +++ b/libervia/web/pages/calls/_browser/__init__.py Mon Aug 14 16:49:02 2023 +0200 @@ -15,15 +15,18 @@ bridge = Bridge() GATHER_TIMEOUT = 10000 ALLOWED_STATUSES = (None, "dialing") -AUDIO = 'audio' -VIDEO = 'video' +AUDIO = "audio" +VIDEO = "video" ALLOWED_CALL_MODES = {AUDIO, VIDEO} +INACTIVE_CLASS = "inactive" +MUTED_CLASS = "muted" +SCREEN_OFF_CLASS = "screen-off" class CallUI: - def __init__(self): self.webrtc = WebRTC() + self.webrtc.screen_sharing_cb = self.on_sharing_screen self.mode = "search" self._status = None self._callee = None @@ -44,42 +47,34 @@ self._call_mode = VIDEO self.call_button_tpl = Template("call/call_button.html") self._update_call_button() - document['toggle_call_mode_btn'].bind('click', self.switch_call_mode) - document["hangup_btn"].bind( - "click", - lambda __: aio.run(self.hang_up()) - ) + document["toggle_call_mode_btn"].bind("click", self.switch_call_mode) + document["hangup_btn"].bind("click", lambda __: aio.run(self.hang_up())) # other buttons - document["full_screen_btn"].bind( - "click", - lambda __: self.toggle_fullscreen() - ) + document["full_screen_btn"].bind("click", lambda __: self.toggle_fullscreen()) document["exit_full_screen_btn"].bind( - "click", - lambda __: self.toggle_fullscreen() + "click", lambda __: self.toggle_fullscreen() ) - document["mute_audio_btn"].bind( - "click", - self.toggle_audio_mute - ) - document["mute_video_btn"].bind( - "click", - self.toggle_video_mute - ) + document["mute_audio_btn"].bind("click", self.toggle_audio_mute) + document["mute_video_btn"].bind("click", self.toggle_video_mute) + self.share_desktop_btn_elt = document["share_desktop_btn"] + if hasattr(window.navigator.mediaDevices, "getDisplayMedia"): + self.share_desktop_btn_elt.classList.remove("is-hidden-touch") + # screen sharing is supported + self.share_desktop_btn_elt.bind("click", self.toggle_screen_sharing) + else: + self.share_desktop_btn_elt.classList.add("is-hidden") # search self.jid_search = JidSearch( - document["search"], - document["contacts"], - click_cb = self._on_entity_click + document["search"], document["contacts"], click_cb=self._on_entity_click ) # incoming call dialog self.incoming_call_dialog_elt = None @property - def sid(self) -> str|None: + def sid(self) -> str | None: return self.webrtc.sid @sid.setter @@ -97,10 +92,7 @@ raise Exception( f"INTERNAL ERROR: this status is not allowed: {new_status!r}" ) - tpl_data = { - "entity": self._callee, - "status": new_status - } + tpl_data = {"entity": self._callee, "status": new_status} if self._callee is not None: try: tpl_data["name"] = cache.identities[self._callee]["nicknames"][0] @@ -110,7 +102,6 @@ self.call_status_wrapper_elt.clear() self.call_status_wrapper_elt <= status_elt - self._status = new_status @property @@ -139,10 +130,9 @@ def _update_call_button(self): new_button = self.call_button_tpl.get_elt({"call_mode": self.call_mode}) new_button.bind( - "click", - lambda __: aio.run(self.make_call(video=not self.call_mode == AUDIO)) + "click", lambda __: aio.run(self.make_call(video=not self.call_mode == AUDIO)) ) - document['call_btn'].replaceWith(new_button) + document["call_btn"].replaceWith(new_button) def _on_action_new( self, action_data_s: str, action_id: str, security_limit: int, profile: str @@ -161,9 +151,7 @@ async def on_action_new(self, action_data: dict, action_id: str) -> None: peer_jid = action_data["from_jid"] - log.info( - f"{peer_jid} wants to start a call ({action_data['sub_type']})" - ) + log.info(f"{peer_jid} wants to start a call ({action_data['sub_type']})") if self.sid is not None: log.warning( f"already in a call ({self.sid}), can't receive a new call from " @@ -173,7 +161,7 @@ sid = self.sid = action_data["session_id"] await cache.fill_identities([peer_jid]) identity = cache.identities[peer_jid] - peer_name = identity['nicknames'][0] + peer_name = identity["nicknames"][0] # we start the ring self.audio_player_elt.play() @@ -181,19 +169,14 @@ # and ask user if we take the call try: self.incoming_call_dialog_elt = dialog.Confirm( - f"{peer_name} is calling you.", - ok_label="Answer", - cancel_label="Reject" + f"{peer_name} is calling you.", ok_label="Answer", cancel_label="Reject" ) accepted = await self.incoming_call_dialog_elt.ashow() except dialog.CancelError: log.info("Call has been cancelled") self.incoming_call_dialog_elt = None self.sid = None - dialog.notification.show( - f"{peer_name} has cancelled the call", - level="info" - ) + dialog.notification.show(f"{peer_name} has cancelled the call", level="info") return self.incoming_call_dialog_elt = None @@ -210,10 +193,7 @@ else: log.info(f"your are declining the call from {peer_jid}") self.sid = None - await bridge.action_launch( - action_id, - json.dumps({"cancelled": not accepted}) - ) + await bridge.action_launch(action_id, json.dumps({"cancelled": not accepted})) def _on_call_ended(self, session_id: str, data_s: str, profile: str) -> None: """Call has been terminated @@ -241,7 +221,9 @@ """ aio.run(self.on_call_setup(session_id, json.loads(setup_data_s), profile)) - async def on_call_setup(self, session_id: str, setup_data: dict, profile: str) -> None: + async def on_call_setup( + self, session_id: str, setup_data: dict, profile: str + ) -> None: """Call has been accepted, connection can be established @param session_id: Session identifier @@ -260,8 +242,7 @@ sdp = setup_data["sdp"] except KeyError: dialog.notification.show( - f"Invalid setup data received: {setup_data}", - level="error" + f"Invalid setup data received: {setup_data}", level="error" ) return if role == "initiator": @@ -270,8 +251,7 @@ await self.webrtc.answer_call(session_id, sdp, profile) else: dialog.notification.show( - f"Invalid role received during setup: {setup_data}", - level="error" + f"Invalid role received during setup: {setup_data}", level="error" ) return @@ -287,22 +267,22 @@ raise ValueError except ValueError: dialog.notification.show( - "Invalid identifier, please use a valid callee identifier", - level="error" + "Invalid identifier, please use a valid callee identifier", level="error" ) return self._callee = callee_jid await cache.fill_identities([callee_jid]) self.status = "dialing" - call_avatar_elt = self.call_avatar_tpl.get_elt({ - "entity": str(callee_jid), - "identities": cache.identities, - }) + call_avatar_elt = self.call_avatar_tpl.get_elt( + { + "entity": str(callee_jid), + "identities": cache.identities, + } + ) self.call_avatar_wrapper_elt.clear() self.call_avatar_wrapper_elt <= call_avatar_elt - self.switch_mode("call") await self.webrtc.make_call(callee_jid, audio, video) @@ -335,16 +315,13 @@ log.warning("Can't hand_up, not call in progress") return await self.end_call({"reason": "terminated"}) - await bridge.call_end( - session_id, - "" - ) + await bridge.call_end(session_id, "") def _handle_animation_end( self, element, - remove = None, - add = None, + remove=None, + add=None, ): """Return a handler that removes specified classes and the event handler. @@ -352,6 +329,7 @@ @param remove: List of class names to remove from the element. @param add: List of class names to add to the element. """ + def handler(__, remove=remove, add=add): log.info(f"animation end OK {element=}") if add: @@ -362,7 +340,7 @@ if isinstance(remove, str): remove = [remove] element.classList.remove(*remove) - element.unbind('animationend', handler) + element.unbind("animationend", handler) return handler @@ -374,21 +352,16 @@ # Hide contacts with fade-out animation and bring up the call box self.search_container_elt.classList.add("fade-out-y") self.search_container_elt.bind( - 'animationend', + "animationend", self._handle_animation_end( - self.search_container_elt, - remove="fade-out-y", - add="is-hidden" - ) + self.search_container_elt, remove="fade-out-y", add="is-hidden" + ), ) self.call_container_elt.classList.remove("is-hidden") self.call_container_elt.classList.add("slide-in") self.call_container_elt.bind( - 'animationend', - self._handle_animation_end( - self.call_container_elt, - remove="slide-in" - ) + "animationend", + self._handle_animation_end(self.call_container_elt, remove="slide-in"), ) self.mode = mode elif mode == "search": @@ -396,26 +369,26 @@ self.search_container_elt.classList.add("fade-out-y", "animation-reverse") self.search_container_elt.classList.remove("is-hidden") self.search_container_elt.bind( - 'animationend', + "animationend", self._handle_animation_end( self.search_container_elt, remove=["fade-out-y", "animation-reverse"], - ) + ), ) self.call_container_elt.classList.add("slide-in", "animation-reverse") self.call_container_elt.bind( - 'animationend', + "animationend", self._handle_animation_end( self.call_container_elt, remove=["slide-in", "animation-reverse"], - add="is-hidden" - ) + add="is-hidden", + ), ) self.mode = mode else: log.error(f"Internal Error: Unknown call mode: {mode}") - def toggle_fullscreen(self, fullscreen: bool|None = None): + def toggle_fullscreen(self, fullscreen: bool | None = None): """Toggle fullscreen mode for video elements. @param fullscreen: if set, determine the fullscreen state; otherwise, @@ -439,8 +412,7 @@ except Exception as e: dialog.notification.show( - f"An error occurred while toggling fullscreen: {e}", - level="error" + f"An error occurred while toggling fullscreen: {e}", level="error" ) def toggle_audio_mute(self, evt): @@ -448,14 +420,14 @@ btn_elt = evt.currentTarget if is_muted: btn_elt.classList.remove("is-success") - btn_elt.classList.add("muted", "is-warning") + btn_elt.classList.add(INACTIVE_CLASS, MUTED_CLASS, "is-warning") dialog.notification.show( f"audio is now muted", level="info", delay=2, ) else: - btn_elt.classList.remove("muted", "is-warning") + btn_elt.classList.remove(INACTIVE_CLASS, MUTED_CLASS, "is-warning") btn_elt.classList.add("is-success") def toggle_video_mute(self, evt): @@ -463,16 +435,29 @@ btn_elt = evt.currentTarget if is_muted: btn_elt.classList.remove("is-success") - btn_elt.classList.add("muted", "is-warning") + btn_elt.classList.add(INACTIVE_CLASS, MUTED_CLASS, "is-warning") dialog.notification.show( f"video is now muted", level="info", delay=2, ) else: - btn_elt.classList.remove("muted", "is-warning") + btn_elt.classList.remove(INACTIVE_CLASS, MUTED_CLASS, "is-warning") btn_elt.classList.add("is-success") + def toggle_screen_sharing(self, evt): + aio.run(self.webrtc.toggle_screen_sharing()) + + def on_sharing_screen(self, sharing: bool) -> None: + """Called when screen sharing state changes""" + share_desktop_btn_elt = self.share_desktop_btn_elt + if sharing: + share_desktop_btn_elt.classList.add("is-danger") + share_desktop_btn_elt.classList.remove(INACTIVE_CLASS, SCREEN_OFF_CLASS) + else: + share_desktop_btn_elt.classList.remove("is-danger") + share_desktop_btn_elt.classList.add(INACTIVE_CLASS, SCREEN_OFF_CLASS) + def _on_entity_click(self, item: dict) -> None: aio.run(self.on_entity_click(item))