Mercurial > libervia-web
comparison libervia/web/pages/calls/_browser/webrtc.py @ 1616:6bfeb9f0fb84
browser (calls): conferences implementation:
- Handle A/V conferences calls creation/joining by entering a conference room JID in the
search box.
- Group call box has been improved and is used both for group calls (small number of
participants) and A/V conferences (larger number of participants).
- Fullscreen button for group call is working.
- Avatar/user nickname are shown in group call on peer user, as an overlay on video
stream.
- Use `user` metadata when present to display the right user avatar/name when receiving a
stream from SFU (i.e. A/V conference).
- Peer user have a new 3 dots menu with a `pin` item to (un)pin it (i.e. display it on
full container with on top).
- Updated webrtc to handle unidirectional streams correctly and to adapt to A/V conference
specification.
rel 448
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 07 Aug 2024 00:01:57 +0200 |
parents | 4a9679369856 |
children |
comparison
equal
deleted
inserted
replaced
1615:97ea776df74c | 1616:6bfeb9f0fb84 |
---|---|
646 | 646 |
647 self._peer_connection = peer_connection | 647 self._peer_connection = peer_connection |
648 window.pc = self._peer_connection | 648 window.pc = self._peer_connection |
649 return peer_connection | 649 return peer_connection |
650 | 650 |
651 async def _get_user_media(self, audio: bool = True, video: bool = True) -> None: | 651 async def _get_user_media( |
652 self, | |
653 audio: bool = True, | |
654 video: bool = True, | |
655 direction: str = "sendrecv" | |
656 ) -> None: | |
652 """ | 657 """ |
653 Gets user media (camera and microphone). | 658 Gets user media (camera and microphone). |
654 | 659 |
655 @param audio: True if an audio flux is required. | 660 @param audio: True if an audio flux is required. |
656 @param video: True if a video flux is required. | 661 @param video: True if a video flux is required. |
662 @param direction: The direction of the stream ('sendonly', 'recvonly', 'sendrecv', | |
663 or 'inactive') | |
657 """ | 664 """ |
658 media_constraints = {"audio": audio, "video": video} | 665 media_constraints = {"audio": audio, "video": video} |
659 if self.local_stream is None: | 666 if self.local_stream is None: |
660 self.local_stream = await window.navigator.mediaDevices.getUserMedia( | 667 self.local_stream = await window.navigator.mediaDevices.getUserMedia( |
661 media_constraints | 668 media_constraints |
666 return | 673 return |
667 | 674 |
668 if self.local_video_elt is not None: | 675 if self.local_video_elt is not None: |
669 self.local_video_elt.srcObject = self.local_stream | 676 self.local_video_elt.srcObject = self.local_stream |
670 | 677 |
671 for track in self.local_stream.getTracks(): | 678 if direction != "recvonly": |
672 self._peer_connection.addTrack(track) | 679 for track in self.local_stream.getTracks(): |
680 sender = self._peer_connection.addTransceiver(track, { | |
681 'direction': direction, | |
682 'streams': [self.local_stream] | |
683 }) | |
673 | 684 |
674 async def _replace_user_video( | 685 async def _replace_user_video( |
675 self, | 686 self, |
676 screen: bool = False, | 687 screen: bool = False, |
677 ) -> JSObject | None: | 688 ) -> JSObject | None: |
934 self.remote_stream = window.MediaStream.new() | 945 self.remote_stream = window.MediaStream.new() |
935 self.remote_video_elt.srcObject = self.remote_stream | 946 self.remote_video_elt.srcObject = self.remote_stream |
936 self.remote_stream.addTrack(event.track) | 947 self.remote_stream.addTrack(event.track) |
937 | 948 |
938 def on_negotiation_needed(self, event) -> None: | 949 def on_negotiation_needed(self, event) -> None: |
939 log.debug(f"on_negotiation_needed {event=}") | 950 log.debug("on_negotiation_needed") |
940 # TODO | 951 # TODO |
941 | 952 |
942 def _on_data_channel(self, event) -> None: | 953 def _on_data_channel(self, event) -> None: |
943 """Handles the data channel event from the peer connection. | 954 """Handles the data channel event from the peer connection. |
944 | 955 |
945 @param event: The event associated with the opening of a data channel. | 956 @param event: The event associated with the opening of a data channel. |
946 """ | 957 """ |
947 data_channel = event.channel | 958 data_channel = event.channel |
948 self.file_receiver = FileReceiver(self.sid, data_channel, self.extra_data) | 959 self.file_receiver = FileReceiver(self.sid, data_channel, self.extra_data) |
949 | 960 |
950 async def answer_call(self, sid: str, offer_sdp: str, profile: str): | 961 async def answer_call( |
962 self, | |
963 sid: str, | |
964 offer_sdp: str, | |
965 profile: str, | |
966 conference: bool = False | |
967 ): | |
951 """We respond to the call""" | 968 """We respond to the call""" |
952 log.debug("answering call") | 969 log.debug("answering call") |
953 if sid != self.sid: | 970 if sid != self.sid: |
954 raise Exception(f"Internal Error: unexpected sid: {sid=} {self.sid=}") | 971 raise Exception(f"Internal Error: unexpected sid: {sid=} {self.sid=}") |
955 await self._create_peer_connection() | 972 await self._create_peer_connection() |
960 await self.on_ice_candidates_new(self.remote_candidates_buffer) | 977 await self.on_ice_candidates_new(self.remote_candidates_buffer) |
961 self.remote_candidates_buffer.clear() | 978 self.remote_candidates_buffer.clear() |
962 if self.file_only: | 979 if self.file_only: |
963 self._peer_connection.bind("datachannel", self._on_data_channel) | 980 self._peer_connection.bind("datachannel", self._on_data_channel) |
964 else: | 981 else: |
965 await self._get_user_media() | 982 await self._get_user_media( |
983 direction="recvonly" if conference else "sendrecv" | |
984 ) | |
966 | 985 |
967 # Gather local ICE candidates | 986 # Gather local ICE candidates |
968 local_ice_data = await self._gather_ice_candidates(False) | 987 local_ice_data = await self._gather_ice_candidates(False) |
969 self.local_candidates = local_ice_data["candidates"] | 988 self.local_candidates = local_ice_data["candidates"] |
970 | 989 |
996 self.local_candidates_buffer.clear() | 1015 self.local_candidates_buffer.clear() |
997 | 1016 |
998 async def prepare_call( | 1017 async def prepare_call( |
999 self, | 1018 self, |
1000 audio: bool = True, | 1019 audio: bool = True, |
1001 video: bool = True | 1020 video: bool = True, |
1021 direction: str = "sendrecv" | |
1002 ) -> dict: | 1022 ) -> dict: |
1003 """Prepare a call. | 1023 """Prepare a call. |
1004 | 1024 |
1005 Create RTCPeerConnection instance, and get use media. | 1025 Create RTCPeerConnection instance, and get use media. |
1006 | 1026 |
1007 @param audio: True if an audio flux is required | 1027 @param audio: True if an audio flux is required |
1008 @param video: True if a video flux is required | 1028 @param video: True if a video flux is required |
1009 @return: Call Data | 1029 @return: Call Data |
1010 """ | 1030 """ |
1011 await self._create_peer_connection() | 1031 await self._create_peer_connection() |
1012 await self._get_user_media(audio, video) | 1032 await self._get_user_media(audio, video, direction) |
1013 return await self._get_call_data() | 1033 return await self._get_call_data() |
1014 | 1034 |
1015 async def make_call( | 1035 async def make_call( |
1016 self, callee_jid: jid.JID, audio: bool = True, video: bool = True | 1036 self, |
1037 callee_jid: jid.JID, | |
1038 audio: bool = True, | |
1039 video: bool = True, | |
1040 direction: str = "sendrecv" | |
1017 ) -> None: | 1041 ) -> None: |
1018 """ | 1042 """ |
1019 @param audio: True if an audio flux is required | 1043 @param audio: True if an audio flux is required |
1020 @param video: True if a video flux is required | 1044 @param video: True if a video flux is required |
1021 """ | 1045 """ |
1022 call_data = await self.prepare_call(audio, video) | 1046 call_data = await self.prepare_call(audio, video, direction) |
1023 log.info(f"calling {callee_jid!r}") | 1047 log.info(f"calling {callee_jid!r}") |
1024 self.sid = await bridge.call_start(str(callee_jid), json.dumps(call_data)) | 1048 self.sid = await bridge.call_start(str(callee_jid), json.dumps(call_data)) |
1025 log.debug(f"Call SID: {self.sid}") | 1049 log.debug(f"Call SID: {self.sid}") |
1026 await self._send_buffered_local_candidates() | 1050 await self._send_buffered_local_candidates() |
1027 | 1051 |