comparison libervia/backend/plugins/plugin_xep_0167/__init__.py @ 4291:39ac821ebbdb

plugin XEP-0167: handle conferences: - SDP can now be answered by component instead of frontend. This is useful for A/V conferences component to handle A/V call jingle sessions. - new `call_update` and method, and `content-add` action preparation. This is not yet used by A/V conference, but it's a preparation for a potential future use. - Add NS_AV_CONFERENCES to features as required by the newly proposed A/V Conferences protoXEP. rel 447
author Goffi <goffi@goffi.org>
date Mon, 29 Jul 2024 03:31:09 +0200
parents 96fdf4891747
children a0ed5c976bf8
comparison
equal deleted inserted replaced
4290:4837ec911c43 4291:39ac821ebbdb
93 out_sign="s", 93 out_sign="s",
94 method=self._call_start, 94 method=self._call_start,
95 async_=True, 95 async_=True,
96 ) 96 )
97 host.bridge.add_method( 97 host.bridge.add_method(
98 "call_update",
99 ".plugin",
100 in_sign="sss",
101 out_sign="",
102 method=self._call_update,
103 async_=True,
104 )
105 host.bridge.add_method(
98 "call_answer_sdp", 106 "call_answer_sdp",
99 ".plugin", 107 ".plugin",
100 in_sign="sss", 108 in_sign="sss",
101 out_sign="", 109 out_sign="",
102 method=self._call_answer_sdp, 110 method=self._call_answer_sdp,
103 async_=True, 111 async_=False,
104 ) 112 )
105 host.bridge.add_method( 113 host.bridge.add_method(
106 "call_info", 114 "call_info",
107 ".plugin", 115 ".plugin",
108 in_sign="ssss", 116 in_sign="ssss",
119 ) 127 )
120 128
121 # args: session_id, serialised setup data (dict with keys "role" and "sdp"), 129 # args: session_id, serialised setup data (dict with keys "role" and "sdp"),
122 # profile 130 # profile
123 host.bridge.add_signal("call_setup", ".plugin", signature="sss") 131 host.bridge.add_signal("call_setup", ".plugin", signature="sss")
132
133 # args: session_id, serialised update data, profile
134 host.bridge.add_signal("call_update", ".plugin", signature="sss")
124 135
125 # args: session_id, data, profile 136 # args: session_id, data, profile
126 host.bridge.add_signal("call_ended", ".plugin", signature="sss") 137 host.bridge.add_signal("call_ended", ".plugin", signature="sss")
127 138
128 # args: session_id, info_type, extra, profile 139 # args: session_id, info_type, extra, profile
142 client = self.host.get_client(profile_key) 153 client = self.host.get_client(profile_key)
143 return defer.ensureDeferred( 154 return defer.ensureDeferred(
144 self.call_start( 155 self.call_start(
145 client, jid.JID(entity_s), data_format.deserialise(call_data_s) 156 client, jid.JID(entity_s), data_format.deserialise(call_data_s)
146 ) 157 )
158 )
159
160 def _call_update(
161 self,
162 session_id: str,
163 call_data_s: str,
164 profile_key: str,
165 ):
166 client = self.host.get_client(profile_key)
167 return defer.ensureDeferred(
168 self.call_update(client, session_id, data_format.deserialise(call_data_s))
147 ) 169 )
148 170
149 def parse_call_data(self, call_data: dict) -> dict: 171 def parse_call_data(self, call_data: dict) -> dict:
150 """Parse ``call_data`` and return corresponding contents end metadata""" 172 """Parse ``call_data`` and return corresponding contents end metadata"""
151 metadata = call_data.get("metadata") or {} 173 metadata = call_data.get("metadata") or {}
231 async def call_start( 253 async def call_start(
232 self, 254 self,
233 client: SatXMPPEntity, 255 client: SatXMPPEntity,
234 peer_jid: jid.JID, 256 peer_jid: jid.JID,
235 call_data: dict, 257 call_data: dict,
258 session_id: str | None = None,
236 ) -> str: 259 ) -> str:
237 """Initiate a call session with the given peer. 260 """Initiate a call session with the given peer.
238 261
239 @param peer_jid: JID of the peer to initiate a call session with. 262 @param peer_jid: JID of the peer to initiate a call session with.
240 @param call_data: Dictionary containing data for the call. Must include SDP information. 263 @param call_data: Dictionary containing data for the call. Must include SDP information.
245 - application_data (dict): Data about the media. 268 - application_data (dict): Data about the media.
246 - fingerprint (str): Security fingerprint data (optional). 269 - fingerprint (str): Security fingerprint data (optional).
247 - id (str): Identifier for the media (optional). 270 - id (str): Identifier for the media (optional).
248 - ice-candidates: ICE candidates for media transport. 271 - ice-candidates: ICE candidates for media transport.
249 - And other transport specific data. 272 - And other transport specific data.
273 @param session_id: ID of the Jingle session. If None, an ID will be automatically
274 generated.
250 275
251 @return: Session ID (SID) for the initiated call session. 276 @return: Session ID (SID) for the initiated call session.
252 277
253 @raises exceptions.DataError: If media data is invalid or duplicate content name 278 @raises exceptions.DataError: If media data is invalid or duplicate content name
254 (mid) is found. 279 (mid) is found.
269 peer_jid, 294 peer_jid,
270 contents, 295 contents,
271 call_type=call_type, 296 call_type=call_type,
272 metadata=metadata, 297 metadata=metadata,
273 peer_metadata={}, 298 peer_metadata={},
299 sid=session_id,
274 ) 300 )
275 return sid 301 return sid
302
303 async def call_update(
304 self,
305 client: SatXMPPEntity,
306 session_id: str,
307 call_data: dict,
308 ) -> None:
309 """Update a running call session.
310
311 @param session_id: ID of the Jingle session to update.
312 @param call_data: Dictionary containing updated data for the call. Must include SDP information.
313 The dict can have the following keys:
314 - sdp (str): SDP data for the call.
315 - metadata (dict): Additional metadata for the call (optional).
316 Each media type ("audio" and "video") in the SDP should have:
317 - application_data (dict): Data about the media.
318 - fingerprint (str): Security fingerprint data (optional).
319 - id (str): Identifier for the media (optional).
320 - ice-candidates: ICE candidates for media transport.
321 - And other transport specific data.
322
323
324 @raises exceptions.DataError: If media data is invalid or duplicate content name
325 (mid) is found.
326 """
327 session = self._j.get_session(client, session_id)
328 try:
329 new_offer_sdp = call_data["sdp"]
330 except KeyError:
331 raise exceptions.DataError(f"New SDP offer is missing: {call_data}")
332 metadata = self.parse_call_data(call_data)
333 contents = self.get_contents(call_data, metadata)
334 if not contents:
335 raise exceptions.DataError("no valid media data found: {call_data}")
336
337 call_type = (
338 C.META_SUBTYPE_CALL_VIDEO
339 if "video" in call_data
340 else C.META_SUBTYPE_CALL_AUDIO
341 )
342 for content_args in contents:
343 content = content_args["app_kwargs"]
344 content["app_ns"] = NS_JINGLE_RTP
345 content["name"] = (content_args["name"],)
346 content["transport_type"] = self._j.TRANSPORT_DATAGRAM
347 media = content["media"]
348 media_data = content["media_data"].copy()
349 media_data["transport_data"] = content_args["transport_data"][
350 "local_ice_data"
351 ]
352 desc_elt = mapping.build_description(media, media_data, {})
353 iq_elt, __ = self._j.build_action(
354 client,
355 self._j.A_CONTENT_ADD,
356 session,
357 content_args["name"],
358 context_elt=desc_elt,
359 )
360 content_data = self._j.get_content_data(content)
361 transport = self._j.get_transport(client, content, content_data)
362 transport_elt = transport.handler.build_transport(
363 media_data["transport_data"]
364 )
365 iq_elt.jingle.content.addChild(transport_elt)
366 await iq_elt.send()
276 367
277 def _call_answer_sdp(self, session_id: str, answer_sdp: str, profile: str) -> None: 368 def _call_answer_sdp(self, session_id: str, answer_sdp: str, profile: str) -> None:
278 client = self.host.get_client(profile) 369 client = self.host.get_client(profile)
279 session = self._j.get_session(client, session_id) 370 session = self._j.get_session(client, session_id)
280 try: 371 try:
549 application_data["local_data"] = media_data["application_data"] 640 application_data["local_data"] = media_data["application_data"]
550 transport_data = content["transport_data"] 641 transport_data = content["transport_data"]
551 local_ice_data = media_data["transport_data"] 642 local_ice_data = media_data["transport_data"]
552 transport_data["local_ice_data"] = local_ice_data 643 transport_data["local_ice_data"] = local_ice_data
553 644
554 def send_answer_sdp(self, client: SatXMPPEntity, session: dict) -> None: 645 async def send_answer_sdp(self, client: SatXMPPEntity, session: dict) -> None:
555 """Send answer SDP to frontend""" 646 """Send answer SDP to frontend"""
556 if not session.get(ANSWER_SDP_SENT_KEY, False): 647 if not session.get(ANSWER_SDP_SENT_KEY, False):
557 # we only send the signal once, as it means that the whole session is 648 # we only send the signal once, as it means that the whole session is
558 # accepted 649 # accepted
559 answer_sdp = mapping.generate_sdp_from_session(session) 650 answer_sdp = mapping.generate_sdp_from_session(session)
560 self.host.bridge.call_setup( 651
561 session["id"], 652 call_setup = session.get("call_setup_cb")
562 data_format.serialise( 653
654 if call_setup is None:
655 self.host.bridge.call_setup(
656 session["id"],
657 data_format.serialise(
658 {
659 "role": session["role"],
660 "sdp": answer_sdp,
661 }
662 ),
663 client.profile,
664 )
665 else:
666 await call_setup(
667 client,
668 session,
563 { 669 {
564 "role": session["role"], 670 "role": session["role"],
565 "sdp": answer_sdp, 671 "sdp": answer_sdp,
566 } 672 },
567 ), 673 )
568 client.profile, 674
569 )
570 session[ANSWER_SDP_SENT_KEY] = True 675 session[ANSWER_SDP_SENT_KEY] = True
571 676
572 async def jingle_handler(self, client, action, session, content_name, desc_elt): 677 async def jingle_handler(self, client, action, session, content_name, desc_elt):
573 content_data = session["contents"][content_name] 678 if action == self._j.A_CONTENT_ADD:
679 content_data = session["contents_new"][content_name]
680 else:
681 content_data = session["contents"][content_name]
574 application_data = content_data["application_data"] 682 application_data = content_data["application_data"]
575 if action == self._j.A_PREPARE_CONFIRMATION: 683 if action == self._j.A_PREPARE_CONFIRMATION:
576 session["metadata"] = {} 684 session["metadata"] = {}
577 session.setdefault("peer_metadata", {}) 685 session.setdefault("peer_metadata", {})
578 try: 686 try:
590 elif action == self._j.A_ACCEPTED_ACK: 698 elif action == self._j.A_ACCEPTED_ACK:
591 pass 699 pass
592 elif action == self._j.A_PREPARE_INITIATOR: 700 elif action == self._j.A_PREPARE_INITIATOR:
593 application_data["peer_data"] = mapping.parse_description(desc_elt) 701 application_data["peer_data"] = mapping.parse_description(desc_elt)
594 elif action == self._j.A_SESSION_ACCEPT: 702 elif action == self._j.A_SESSION_ACCEPT:
595 self.send_answer_sdp(client, session) 703 await self.send_answer_sdp(client, session)
704 elif action == self._j.A_CONTENT_ADD:
705 current_contents = session["contents"]
706 if content_name in current_contents:
707 raise exceptions.ConflictError(
708 f"There is already a {content_name!r} content."
709 )
710 current_contents[content_name] = content_data
711 application_data["media"] = desc_elt["media"]
712 application_data["peer_data"] = mapping.parse_description(desc_elt)
596 else: 713 else:
597 log.warning(f"FIXME: unmanaged action {action}") 714 log.warning(f"FIXME: unmanaged action {action}")
598 715
599 await self.host.trigger.async_point( 716 await self.host.trigger.async_point(
600 "XEP-0167_jingle_handler", 717 "XEP-0167_jingle_handler",
676 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 793 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
677 return [ 794 return [
678 disco.DiscoFeature(NS_JINGLE_RTP), 795 disco.DiscoFeature(NS_JINGLE_RTP),
679 disco.DiscoFeature(NS_JINGLE_RTP_AUDIO), 796 disco.DiscoFeature(NS_JINGLE_RTP_AUDIO),
680 disco.DiscoFeature(NS_JINGLE_RTP_VIDEO), 797 disco.DiscoFeature(NS_JINGLE_RTP_VIDEO),
798 disco.DiscoFeature(NS_AV_CONFERENCES),
681 ] 799 ]
682 800
683 def getDiscoItems(self, requestor, target, nodeIdentifier=""): 801 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
684 return [] 802 return []