comparison libervia/backend/plugins/plugin_xep_0167/__init__.py @ 4231:e11b13418ba6

plugin XEP-0353, XEP-0234, jingle: WebRTC data channel signaling implementation: Implement XEP-0343: Signaling WebRTC Data Channels in Jingle. The current version of the XEP (0.3.1) has no implementation and contains some flaws. After discussing this on xsf@, Daniel (from Conversations) mentioned that they had a sprint with Larma (from Dino) to work on another version and provided me with this link: https://gist.github.com/iNPUTmice/6c56f3e948cca517c5fb129016d99e74 . I have used it for my implementation. This implementation reuses work done on Jingle A/V call (notably XEP-0176 and XEP-0167 plugins), with adaptations. When used, XEP-0234 will not handle the file itself as it normally does. This is because WebRTC has several implementations (browser for web interface, GStreamer for others), and file/data must be handled directly by the frontend. This is particularly important for web frontends, as the file is not sent from the backend but from the end-user's browser device. Among the changes, there are: - XEP-0343 implementation. - `file_send` bridge method now use serialised dict as output. - New `BaseTransportHandler.is_usable` method which get content data and returns a boolean (default to `True`) to tell if this transport can actually be used in this context (when we are initiator). Used in webRTC case to see if call data are available. - Support of `application` media type, and everything necessary to handle data channels. - Better confirmation message, with file name, size and description when available. - When file is accepted in preflight, it is specified in following `action_new` signal for actual file transfer. This way, frontend can avoid the display or 2 confirmation messages. - XEP-0166: when not specified, default `content` name is now its index number instead of a UUID. This follows the behaviour of browsers. - XEP-0353: better handling of events such as call taken by another device. - various other updates. rel 441
author Goffi <goffi@goffi.org>
date Sat, 06 Apr 2024 12:57:23 +0200
parents 832a7bdb3aea
children 79c8a70e1813
comparison
equal deleted inserted replaced
4230:314d3c02bb67 4231:e11b13418ba6
79 self.host = host 79 self.host = host
80 # FIXME: to be removed once host is accessible from global var 80 # FIXME: to be removed once host is accessible from global var
81 mapping.host = host 81 mapping.host = host
82 self._j = host.plugins["XEP-0166"] 82 self._j = host.plugins["XEP-0166"]
83 self._j.register_application(NS_JINGLE_RTP, self) 83 self._j.register_application(NS_JINGLE_RTP, self)
84 host.register_namespace("jingle-rtp", NS_JINGLE_RTP)
84 host.bridge.add_method( 85 host.bridge.add_method(
85 "call_start", 86 "call_start",
86 ".plugin", 87 ".plugin",
87 in_sign="sss", 88 in_sign="sss",
88 out_sign="s", 89 out_sign="s",
139 self.call_start( 140 self.call_start(
140 client, jid.JID(entity_s), data_format.deserialise(call_data_s) 141 client, jid.JID(entity_s), data_format.deserialise(call_data_s)
141 ) 142 )
142 ) 143 )
143 144
144 async def call_start( 145 def parse_call_data(self, call_data: dict) -> dict:
145 self, 146 """Parse ``call_data`` and return corresponding contents end metadata"""
146 client: SatXMPPEntity,
147 peer_jid: jid.JID,
148 call_data: dict,
149 ) -> str:
150 """Initiate a call session with the given peer.
151
152 @param peer_jid: JID of the peer to initiate a call session with.
153 @param call_data: Dictionary containing data for the call. Must include SDP information.
154 The dict can have the following keys:
155 - sdp (str): SDP data for the call.
156 - metadata (dict): Additional metadata for the call (optional).
157 Each media type ("audio" and "video") in the SDP should have:
158 - application_data (dict): Data about the media.
159 - fingerprint (str): Security fingerprint data (optional).
160 - id (str): Identifier for the media (optional).
161 - ice-candidates: ICE candidates for media transport.
162 - And other transport specific data.
163
164 @return: Session ID (SID) for the initiated call session.
165
166 @raises exceptions.DataError: If media data is invalid or duplicate content name
167 (mid) is found.
168 """
169 contents = []
170 metadata = call_data.get("metadata") or {} 147 metadata = call_data.get("metadata") or {}
171 148
172 if "sdp" in call_data: 149 if "sdp" in call_data:
173 sdp_data = mapping.parse_sdp(call_data["sdp"]) 150 sdp_data = mapping.parse_sdp(call_data["sdp"])
174 to_delete = set() 151 to_delete = set()
175 for media, data in sdp_data.items(): 152 for media, data in sdp_data.items():
176 if media not in ("audio", "video"): 153 if media not in ("audio", "video", "application"):
177 continue 154 continue
178 to_delete.add(media) 155 to_delete.add(media)
179 media_type, media_data = media, data 156 media_type, media_data = media, data
180 call_data[media_type] = media_data["application_data"] 157 call_data[media_type] = media_data["application_data"]
181 transport_data = media_data["transport_data"] 158 transport_data = media_data["transport_data"]
186 pass 163 pass
187 try: 164 try:
188 call_data[media_type]["id"] = media_data["id"] 165 call_data[media_type]["id"] = media_data["id"]
189 except KeyError: 166 except KeyError:
190 log.warning(f"no media ID found for {media_type}: {media_data}") 167 log.warning(f"no media ID found for {media_type}: {media_data}")
168 # FIXME: the 2 values below are linked to XEP-0343, they should be added
169 # there instead, maybe with some new trigger?
170 for key in ("sctp-port","max-message-size"):
171 value = transport_data.get(key)
172 if value is not None:
173 metadata[key] = value
191 try: 174 try:
192 call_data[media_type]["ice-candidates"] = transport_data.get( 175 call_data[media_type]["ice-candidates"] = transport_data.get(
193 "candidates", [] 176 "candidates", []
194 ) 177 )
195 metadata["ice-ufrag"] = transport_data["ufrag"] 178 metadata["ice-ufrag"] = transport_data["ufrag"]
199 continue 182 continue
200 for media in to_delete: 183 for media in to_delete:
201 del sdp_data[media] 184 del sdp_data[media]
202 metadata.update(sdp_data.get("metadata", {})) 185 metadata.update(sdp_data.get("metadata", {}))
203 186
204 call_type = ( 187 return metadata
205 C.META_SUBTYPE_CALL_VIDEO 188
206 if "video" in call_data 189 async def call_start(
207 else C.META_SUBTYPE_CALL_AUDIO 190 self,
208 ) 191 client: SatXMPPEntity,
192 peer_jid: jid.JID,
193 call_data: dict,
194 ) -> str:
195 """Initiate a call session with the given peer.
196
197 @param peer_jid: JID of the peer to initiate a call session with.
198 @param call_data: Dictionary containing data for the call. Must include SDP information.
199 The dict can have the following keys:
200 - sdp (str): SDP data for the call.
201 - metadata (dict): Additional metadata for the call (optional).
202 Each media type ("audio" and "video") in the SDP should have:
203 - application_data (dict): Data about the media.
204 - fingerprint (str): Security fingerprint data (optional).
205 - id (str): Identifier for the media (optional).
206 - ice-candidates: ICE candidates for media transport.
207 - And other transport specific data.
208
209 @return: Session ID (SID) for the initiated call session.
210
211 @raises exceptions.DataError: If media data is invalid or duplicate content name
212 (mid) is found.
213 """
214 sid = str(uuid.uuid4())
215 metadata = self.parse_call_data(call_data)
216 contents = []
209 seen_names = set() 217 seen_names = set()
210 218
211 for media, media_data in call_data.items(): 219 for media, media_data in call_data.items():
212 if media not in ("audio", "video"): 220 if media not in ("audio", "video"):
213 continue 221 continue
233 ) 241 )
234 content["name"] = name 242 content["name"] = name
235 contents.append(content) 243 contents.append(content)
236 if not contents: 244 if not contents:
237 raise exceptions.DataError("no valid media data found: {call_data}") 245 raise exceptions.DataError("no valid media data found: {call_data}")
238 sid = str(uuid.uuid4()) 246
247 call_type = (
248 C.META_SUBTYPE_CALL_VIDEO if "video" in call_data
249 else C.META_SUBTYPE_CALL_AUDIO
250 )
251
239 defer.ensureDeferred( 252 defer.ensureDeferred(
240 self._j.initiate( 253 self._j.initiate(
241 client, 254 client,
242 peer_jid, 255 peer_jid,
243 contents, 256 contents,
293 ) -> bool: 306 ) -> bool:
294 """Prompt the user for a call confirmation. 307 """Prompt the user for a call confirmation.
295 308
296 @param client: The client entity. 309 @param client: The client entity.
297 @param session: The Jingle session. 310 @param session: The Jingle session.
298 @param media_type: Type of media (audio or video). 311 @param call_type: Type of media (audio or video).
299 312
300 @return: True if the call has been accepted 313 @return: True if the call has been accepted
301 """ 314 """
302 peer_jid = session["peer_jid"] 315 peer_jid = session["peer_jid"]
303 316
391 404
392 async def jingle_preflight_cancel( 405 async def jingle_preflight_cancel(
393 self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError 406 self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError
394 ) -> None: 407 ) -> None:
395 """The call has been rejected""" 408 """The call has been rejected"""
396 # call_ended is use to send the signal only once even if there are audio and video 409 # call_ended is used to send the signal only once even if there are audio and
397 # contents 410 # video contents
398 call_ended = session.get("call_ended", False) 411 call_ended = session.get("call_ended", False)
399 if call_ended: 412 if call_ended:
400 return 413 return
401 data = {"reason": getattr(cancel_error, "reason", "cancelled")} 414 data = {"reason": getattr(cancel_error, "reason", None) or "cancelled"}
402 text = getattr(cancel_error, "text", None) 415 data["text"] = str(cancel_error)
403 if text:
404 data["text"] = text
405 self.host.bridge.call_ended( 416 self.host.bridge.call_ended(
406 session["id"], data_format.serialise(data), client.profile 417 session["id"], data_format.serialise(data), client.profile
407 ) 418 )
408 session["call_ended"] = True 419 session["call_ended"] = True
409 420