Mercurial > libervia-backend
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 [] |