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