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