312 lines
12 KiB
Python
312 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Libervia plugin
|
|
# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from typing import List
|
|
|
|
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
|
from twisted.words.xish import domish
|
|
from wokkel import disco, iwokkel
|
|
from zope.interface import implementer
|
|
|
|
from sat.core.constants import Const as C
|
|
from sat.core.i18n import _
|
|
from sat.core.log import getLogger
|
|
|
|
log = getLogger(__name__)
|
|
|
|
NS_JINGLE_RTP_RTCP_FB = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "Jingle RTP Feedback Negotiation",
|
|
C.PI_IMPORT_NAME: "XEP-0293",
|
|
C.PI_TYPE: "XEP",
|
|
C.PI_MODES: C.PLUG_MODE_BOTH,
|
|
C.PI_PROTOCOLS: ["XEP-0293"],
|
|
C.PI_DEPENDENCIES: ["XEP-0092", "XEP-0166", "XEP-0167"],
|
|
C.PI_RECOMMENDATIONS: [],
|
|
C.PI_MAIN: "XEP_0293",
|
|
C.PI_HANDLER: "yes",
|
|
C.PI_DESCRIPTION: _("""Jingle RTP Feedback Negotiation"""),
|
|
}
|
|
|
|
RTCP_FB_KEY = "rtcp-fb"
|
|
|
|
|
|
class XEP_0293:
|
|
def __init__(self, host):
|
|
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
|
|
host.trigger.add("XEP-0167_parse_sdp_a", self._parse_sdp_a_trigger)
|
|
host.trigger.add(
|
|
"XEP-0167_generate_sdp_content", self._generate_sdp_content_trigger
|
|
)
|
|
host.trigger.add("XEP-0167_parse_description", self._parse_description_trigger)
|
|
host.trigger.add(
|
|
"XEP-0167_parse_description_payload_type",
|
|
self._parse_description_payload_type_trigger,
|
|
)
|
|
host.trigger.add("XEP-0167_build_description", self._build_description_trigger)
|
|
host.trigger.add(
|
|
"XEP-0167_build_description_payload_type",
|
|
self._build_description_payload_type_trigger,
|
|
)
|
|
|
|
def get_handler(self, client):
|
|
return XEP_0293_handler()
|
|
|
|
## SDP
|
|
|
|
def _parse_sdp_a_trigger(
|
|
self,
|
|
attribute: str,
|
|
parts: List[str],
|
|
call_data: dict,
|
|
metadata: dict,
|
|
media_type: str,
|
|
application_data: dict,
|
|
transport_data: dict,
|
|
) -> None:
|
|
"""Parse "rtcp-fb" and "rtcp-fb-trr-int" attributes
|
|
|
|
@param attribute: The attribute being parsed.
|
|
@param parts: The list of parts in the attribute.
|
|
@param call_data: The call data dict.
|
|
@param metadata: The metadata dict.
|
|
@param media_type: The media type (e.g., audio, video).
|
|
@param application_data: The application data dict.
|
|
@param transport_data: The transport data dict.
|
|
@param payload_map: The payload map dict.
|
|
"""
|
|
if attribute == "rtcp-fb":
|
|
pt_id = parts[0]
|
|
feedback_type = parts[1]
|
|
|
|
feedback_subtype = None
|
|
parameters = {}
|
|
|
|
# Check if there are extra parameters
|
|
if len(parts) > 2:
|
|
feedback_subtype = parts[2]
|
|
|
|
if len(parts) > 3:
|
|
for parameter in parts[3:]:
|
|
name, _, value = parameter.partition("=")
|
|
parameters[name] = value or None
|
|
|
|
# Check if this feedback is linked to a payload type
|
|
if pt_id == "*":
|
|
# Not linked to a payload type, add to application data
|
|
application_data.setdefault(RTCP_FB_KEY, []).append(
|
|
(feedback_type, feedback_subtype, parameters)
|
|
)
|
|
else:
|
|
payload_types = application_data.get("payload_types", {})
|
|
try:
|
|
payload_type = payload_types[int(pt_id)]
|
|
except KeyError:
|
|
log.warning(
|
|
f"Got reference to unknown payload type {pt_id}: "
|
|
f"{' '.join(parts)}"
|
|
)
|
|
else:
|
|
# Linked to a payload type, add to payload data
|
|
payload_type.setdefault(RTCP_FB_KEY, []).append(
|
|
(feedback_type, feedback_subtype, parameters)
|
|
)
|
|
|
|
elif attribute == "rtcp-fb-trr-int":
|
|
pt_id = parts[0] # Payload type ID
|
|
interval = int(parts[1])
|
|
|
|
# Check if this interval is linked to a payload type
|
|
if pt_id == "*":
|
|
# Not linked to a payload type, add to application data
|
|
application_data["rtcp-fb-trr-int"] = interval
|
|
else:
|
|
payload_types = application_data.get("payload_types", {})
|
|
try:
|
|
payload_type = payload_types[int(pt_id)]
|
|
except KeyError:
|
|
log.warning(
|
|
f"Got reference to unknown payload type {pt_id}: "
|
|
f"{' '.join(parts)}"
|
|
)
|
|
else:
|
|
# Linked to a payload type, add to payload data
|
|
payload_type["rtcp-fb-trr-int"] = interval
|
|
|
|
def _generate_rtcp_fb_lines(
|
|
self, data: dict, pt_id: str, sdp_lines: List[str]
|
|
) -> None:
|
|
for type_, subtype, parameters in data.get(RTCP_FB_KEY, []):
|
|
parameters_strs = [
|
|
f"{k}={v}" if v is not None else k for k, v in parameters.items()
|
|
]
|
|
parameters_str = " ".join(parameters_strs)
|
|
|
|
sdp_line = f"a=rtcp-fb:{pt_id} {type_}"
|
|
if subtype:
|
|
sdp_line += f" {subtype}"
|
|
if parameters_str:
|
|
sdp_line += f" {parameters_str}"
|
|
sdp_lines.append(sdp_line)
|
|
|
|
def _generate_rtcp_fb_trr_int_lines(
|
|
self, data: dict, pt_id: str, sdp_lines: List[str]
|
|
) -> None:
|
|
if "rtcp-fb-trr-int" not in data:
|
|
return
|
|
sdp_lines.append(f"a=rtcp-fb:{pt_id} trr-int {data['rtcp-fb-trr-int']}")
|
|
|
|
def _generate_sdp_content_trigger(
|
|
self,
|
|
session: dict,
|
|
local: bool,
|
|
content_name: str,
|
|
content_data: dict,
|
|
sdp_lines: List[str],
|
|
application_data: dict,
|
|
app_data_key: str,
|
|
media_data: dict,
|
|
media: str,
|
|
) -> None:
|
|
"""Generate SDP attributes "rtcp-fb" and "rtcp-fb-trr-int" from application data.
|
|
|
|
@param session: The session data.
|
|
@param local: Whether this is local or remote content.
|
|
@param content_name: The name of the content.
|
|
@param content_data: The data of the content.
|
|
@param sdp_lines: The list of SDP lines to append to.
|
|
@param application_data: The application data dict.
|
|
@param app_data_key: The key for the application data.
|
|
@param media_data: The media data dict.
|
|
@param media: The media type (e.g., audio, video).
|
|
"""
|
|
# Generate lines for application data
|
|
self._generate_rtcp_fb_lines(application_data, "*", sdp_lines)
|
|
self._generate_rtcp_fb_trr_int_lines(application_data, "*", sdp_lines)
|
|
|
|
# Generate lines for each payload type
|
|
for pt_id, payload_data in media_data.get("payload_types", {}).items():
|
|
self._generate_rtcp_fb_lines(payload_data, pt_id, sdp_lines)
|
|
self._generate_rtcp_fb_trr_int_lines(payload_data, pt_id, sdp_lines)
|
|
|
|
## XML
|
|
|
|
def _parse_rtcp_fb_elements(self, parent_elt: domish.Element, data: dict) -> None:
|
|
"""Parse the <rtcp-fb> and <rtcp-fb-trr-int> elements.
|
|
|
|
@param parent_elt: The parent domish.Element.
|
|
@param data: The data dict to populate.
|
|
"""
|
|
for rtcp_fb_elt in parent_elt.elements(NS_JINGLE_RTP_RTCP_FB, "rtcp-fb"):
|
|
try:
|
|
type_ = rtcp_fb_elt["type"]
|
|
subtype = rtcp_fb_elt.getAttribute("subtype")
|
|
|
|
parameters = {}
|
|
for parameter_elt in rtcp_fb_elt.elements(
|
|
NS_JINGLE_RTP_RTCP_FB, "parameter"
|
|
):
|
|
parameters[parameter_elt["name"]] = parameter_elt.getAttribute(
|
|
"value"
|
|
)
|
|
|
|
data.setdefault(RTCP_FB_KEY, []).append((type_, subtype, parameters))
|
|
except (KeyError, ValueError) as e:
|
|
log.warning(f"Error while parsing <rtcp-fb>: {e}\n{rtcp_fb_elt.toXml()}")
|
|
|
|
for rtcp_fb_trr_int_elt in parent_elt.elements(
|
|
NS_JINGLE_RTP_RTCP_FB, "rtcp-fb-trr-int"
|
|
):
|
|
try:
|
|
interval_value = int(rtcp_fb_trr_int_elt["value"])
|
|
data.setdefault("rtcp_fb_trr_int", []).append(interval_value)
|
|
except (KeyError, ValueError) as e:
|
|
log.warning(
|
|
f"Error while parsing <rtcp-fb-trr-int>: {e}\n"
|
|
f"{rtcp_fb_trr_int_elt.toXml()}"
|
|
)
|
|
|
|
def _parse_description_trigger(
|
|
self, desc_elt: domish.Element, media_data: dict
|
|
) -> None:
|
|
"""Parse the <rtcp-fb> and <rtcp-fb-trr-int> elements from a description.
|
|
|
|
@param desc_elt: The <description> domish.Element.
|
|
@param media_data: The media data dict to populate.
|
|
"""
|
|
self._parse_rtcp_fb_elements(desc_elt, media_data)
|
|
|
|
def _parse_description_payload_type_trigger(
|
|
self,
|
|
desc_elt: domish.Element,
|
|
media_data: dict,
|
|
payload_type_elt: domish.Element,
|
|
payload_type_data: dict,
|
|
) -> None:
|
|
"""Parse the <rtcp-fb> and <rtcp-fb-trr-int> elements from a payload type.
|
|
|
|
@param desc_elt: The <description> domish.Element.
|
|
@param media_data: The media data dict.
|
|
@param payload_type_elt: The <payload-type> domish.Element.
|
|
@param payload_type_data: The payload type data dict to populate.
|
|
"""
|
|
self._parse_rtcp_fb_elements(payload_type_elt, payload_type_data)
|
|
|
|
def build_rtcp_fb_elements(self, parent_elt: domish.Element, data: dict) -> None:
|
|
"""Helper method to build the <rtcp-fb> and <rtcp-fb-trr-int> elements"""
|
|
for type_, subtype, parameters in data.get(RTCP_FB_KEY, []):
|
|
rtcp_fb_elt = parent_elt.addElement((NS_JINGLE_RTP_RTCP_FB, "rtcp-fb"))
|
|
rtcp_fb_elt["type"] = type_
|
|
if subtype:
|
|
rtcp_fb_elt["subtype"] = subtype
|
|
for name, value in parameters.items():
|
|
param_elt = rtcp_fb_elt.addElement(name)
|
|
if value is not None:
|
|
param_elt.addContent(str(value))
|
|
|
|
if "rtcp-fb-trr-int" in data:
|
|
rtcp_fb_trr_int_elt = parent_elt.addElement(
|
|
(NS_JINGLE_RTP_RTCP_FB, "rtcp-fb-trr-int")
|
|
)
|
|
rtcp_fb_trr_int_elt["value"] = str(data["rtcp-fb-trr-int"])
|
|
|
|
def _build_description_payload_type_trigger(
|
|
self,
|
|
desc_elt: domish.Element,
|
|
media_data: dict,
|
|
payload_type: dict,
|
|
payload_type_elt: domish.Element,
|
|
) -> None:
|
|
"""Build the <rtcp-fb> and <rtcp-fb-trr-int> elements for a payload type"""
|
|
self.build_rtcp_fb_elements(payload_type_elt, payload_type)
|
|
|
|
def _build_description_trigger(
|
|
self, desc_elt: domish.Element, media_data: dict, session: dict
|
|
) -> None:
|
|
"""Build the <rtcp-fb> and <rtcp-fb-trr-int> elements for a media description"""
|
|
self.build_rtcp_fb_elements(desc_elt, media_data)
|
|
|
|
|
|
@implementer(iwokkel.IDisco)
|
|
class XEP_0293_handler(XMPPHandler):
|
|
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
|
|
return [disco.DiscoFeature(NS_JINGLE_RTP_RTCP_FB)]
|
|
|
|
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
|
|
return []
|