plugin XEP-0167: fix session level SDP attributes parsing:
rel 447
This commit is contained in:
parent
31c80d3626
commit
f637be1e4f
1 changed files with 137 additions and 140 deletions
|
|
@ -284,12 +284,11 @@ def parse_sdp(sdp: str, role: str) -> dict:
|
|||
@param role: Role of the entities which produces the SDP.
|
||||
@return: A dictionary containing parsed session data.
|
||||
"""
|
||||
# FIXME: to be removed once host is accessible from global var
|
||||
assert host is not None
|
||||
lines = sdp.strip().split("\r\n")
|
||||
# session metadata
|
||||
metadata: Dict[str, Any] = {}
|
||||
call_data = {"metadata": metadata}
|
||||
session_attributes: Dict[str, Any] = {}
|
||||
|
||||
media_type = None
|
||||
media_data: Optional[Dict[str, Any]] = None
|
||||
|
|
@ -299,14 +298,13 @@ def parse_sdp(sdp: str, role: str) -> dict:
|
|||
ice_pwd: Optional[str] = None
|
||||
ice_ufrag: Optional[str] = None
|
||||
payload_types: Optional[Dict[int, dict]] = None
|
||||
# default value, will be be modified by SDP
|
||||
senders: str = "both"
|
||||
|
||||
for line in lines:
|
||||
try:
|
||||
parts = line.split()
|
||||
prefix = parts[0][:2] # Extract the 'a=', 'm=', etc., prefix
|
||||
parts[0] = parts[0][2:] # Remove the prefix from the first element
|
||||
prefix = parts[0][:2]
|
||||
parts[0] = parts[0][2:]
|
||||
|
||||
if prefix == "m=":
|
||||
media_type = parts[0]
|
||||
|
|
@ -336,134 +334,140 @@ def parse_sdp(sdp: str, role: str) -> dict:
|
|||
"senders": senders,
|
||||
}
|
||||
|
||||
# Apply session attributes to the new media
|
||||
for attr, value in session_attributes.items():
|
||||
if attr in ("fingerprint", "ice-pwd", "ice-ufrag"):
|
||||
transport_data[attr] = value
|
||||
elif attr in ("sendrecv", "sendonly", "recvonly", "inactive"):
|
||||
application_data["senders"] = value
|
||||
else:
|
||||
application_data[attr] = value
|
||||
|
||||
elif prefix == "a=":
|
||||
if ":" in parts[0]:
|
||||
attribute, parts[0] = parts[0].split(":", 1)
|
||||
else:
|
||||
attribute = parts[0]
|
||||
|
||||
if (
|
||||
media_type is None
|
||||
or application_data is None
|
||||
or transport_data is None
|
||||
) and not (
|
||||
attribute
|
||||
in (
|
||||
"sendrecv",
|
||||
"sendonly",
|
||||
"recvonly",
|
||||
"inactive",
|
||||
"fingerprint",
|
||||
"group",
|
||||
"ice-options",
|
||||
"msid-semantic",
|
||||
"ice-pwd",
|
||||
"ice-ufrag",
|
||||
)
|
||||
):
|
||||
log.warning(
|
||||
"Received attribute before media description, this is "
|
||||
f"invalid: {line}"
|
||||
)
|
||||
continue
|
||||
|
||||
if attribute == "mid":
|
||||
assert media_data is not None
|
||||
try:
|
||||
media_data["id"] = parts[0]
|
||||
except IndexError:
|
||||
log.warning(f"invalid media ID: {line}")
|
||||
|
||||
elif attribute == "rtpmap":
|
||||
assert application_data is not None
|
||||
assert payload_types is not None
|
||||
pt_id = int(parts[0])
|
||||
codec_info = parts[1].split("/")
|
||||
codec = codec_info[0]
|
||||
clockrate = int(codec_info[1])
|
||||
payload_type = {
|
||||
"id": pt_id,
|
||||
"name": codec,
|
||||
"clockrate": clockrate,
|
||||
}
|
||||
# Handle optional channel count
|
||||
if len(codec_info) > 2:
|
||||
channels = int(codec_info[2])
|
||||
payload_type["channels"] = channels
|
||||
|
||||
payload_types.setdefault(pt_id, {}).update(payload_type)
|
||||
|
||||
elif attribute == "fmtp":
|
||||
assert payload_types is not None
|
||||
pt_id = int(parts[0])
|
||||
params = parts[1].split(";")
|
||||
try:
|
||||
payload_type = payload_types[pt_id]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Can find content type {pt_id}, ignoring: {line}"
|
||||
)
|
||||
|
||||
try:
|
||||
payload_type["parameters"] = {
|
||||
name: value
|
||||
for name, value in (param.split("=") for param in params)
|
||||
if media_type is None:
|
||||
# This is a session-level attribute
|
||||
if attribute == "fingerprint":
|
||||
algorithm, fingerprint = parts[0], parts[1]
|
||||
session_attributes["fingerprint"] = {
|
||||
"hash": algorithm,
|
||||
"fingerprint": fingerprint,
|
||||
}
|
||||
except ValueError:
|
||||
payload_type.setdefault("exra-parameters", []).extend(params)
|
||||
|
||||
elif attribute == "candidate":
|
||||
assert transport_data is not None
|
||||
candidate = parse_candidate(parts)
|
||||
|
||||
transport_data.setdefault("candidates", []).append(candidate)
|
||||
|
||||
elif attribute == "fingerprint":
|
||||
algorithm, fingerprint = parts[0], parts[1]
|
||||
fingerprint_data = {"hash": algorithm, "fingerprint": fingerprint}
|
||||
if transport_data is not None:
|
||||
transport_data.setdefault("fingerprint", {}).update(
|
||||
fingerprint_data
|
||||
)
|
||||
elif attribute == "setup":
|
||||
assert transport_data is not None
|
||||
setup = parts[0]
|
||||
transport_data.setdefault("fingerprint", {})["setup"] = setup
|
||||
|
||||
elif attribute == "b":
|
||||
assert application_data is not None
|
||||
bandwidth = int(parts[0])
|
||||
application_data["bandwidth"] = bandwidth
|
||||
|
||||
elif attribute == "rtcp-mux":
|
||||
assert application_data is not None
|
||||
application_data["rtcp-mux"] = True
|
||||
|
||||
elif attribute == "ice-ufrag":
|
||||
if transport_data is not None:
|
||||
transport_data["ufrag"] = parts[0]
|
||||
|
||||
elif attribute == "ice-pwd":
|
||||
if transport_data is not None:
|
||||
transport_data["pwd"] = parts[0]
|
||||
|
||||
elif attribute in ("sendrecv", "sendonly", "recvonly", "inactive"):
|
||||
if attribute == "sendrecv":
|
||||
value = "both"
|
||||
elif attribute == "sendonly":
|
||||
value = role
|
||||
elif attribute == "recvonly":
|
||||
value = "responder" if role == "initiator" else "initiator"
|
||||
elif attribute == "ice-pwd":
|
||||
session_attributes["ice-pwd"] = parts[0]
|
||||
elif attribute == "ice-ufrag":
|
||||
session_attributes["ice-ufrag"] = parts[0]
|
||||
elif attribute in ("sendrecv", "sendonly", "recvonly", "inactive"):
|
||||
if attribute == "sendrecv":
|
||||
value = "both"
|
||||
elif attribute == "sendonly":
|
||||
value = role
|
||||
elif attribute == "recvonly":
|
||||
value = "responder" if role == "initiator" else "initiator"
|
||||
else:
|
||||
value = "none"
|
||||
session_attributes[attribute] = value
|
||||
else:
|
||||
value = "none"
|
||||
session_attributes[attribute] = parts[0] if parts else True
|
||||
else:
|
||||
# This is a media-level attribute
|
||||
if attribute == "mid":
|
||||
assert media_data is not None
|
||||
try:
|
||||
media_data["id"] = parts[0]
|
||||
except IndexError:
|
||||
log.warning(f"invalid media ID: {line}")
|
||||
|
||||
if application_data is None:
|
||||
# this is a global value, we use is as new default value
|
||||
senders = value
|
||||
else:
|
||||
# this is a media specific value, it will replace the one used as
|
||||
# default for this media
|
||||
application_data["senders"] = value
|
||||
elif attribute == "rtpmap":
|
||||
assert application_data is not None
|
||||
assert payload_types is not None
|
||||
pt_id = int(parts[0])
|
||||
codec_info = parts[1].split("/")
|
||||
codec = codec_info[0]
|
||||
clockrate = int(codec_info[1])
|
||||
payload_type = {
|
||||
"id": pt_id,
|
||||
"name": codec,
|
||||
"clockrate": clockrate,
|
||||
}
|
||||
if len(codec_info) > 2:
|
||||
channels = int(codec_info[2])
|
||||
payload_type["channels"] = channels
|
||||
|
||||
payload_types.setdefault(pt_id, {}).update(payload_type)
|
||||
|
||||
elif attribute == "fmtp":
|
||||
assert payload_types is not None
|
||||
pt_id = int(parts[0])
|
||||
params = parts[1].split(";")
|
||||
try:
|
||||
payload_type = payload_types[pt_id]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Can find content type {pt_id}, ignoring: {line}"
|
||||
)
|
||||
|
||||
try:
|
||||
payload_type["parameters"] = {
|
||||
name: value
|
||||
for name, value in (param.split("=") for param in params)
|
||||
}
|
||||
except ValueError:
|
||||
payload_type.setdefault("exra-parameters", []).extend(params)
|
||||
|
||||
elif attribute == "candidate":
|
||||
assert transport_data is not None
|
||||
candidate = parse_candidate(parts)
|
||||
|
||||
transport_data.setdefault("candidates", []).append(candidate)
|
||||
|
||||
elif attribute == "fingerprint":
|
||||
algorithm, fingerprint = parts[0], parts[1]
|
||||
fingerprint_data = {"hash": algorithm, "fingerprint": fingerprint}
|
||||
if transport_data is not None:
|
||||
transport_data.setdefault("fingerprint", {}).update(
|
||||
fingerprint_data
|
||||
)
|
||||
elif attribute == "setup":
|
||||
assert transport_data is not None
|
||||
setup = parts[0]
|
||||
transport_data.setdefault("fingerprint", {})["setup"] = setup
|
||||
|
||||
elif attribute == "b":
|
||||
assert application_data is not None
|
||||
bandwidth = int(parts[0])
|
||||
application_data["bandwidth"] = bandwidth
|
||||
|
||||
elif attribute == "rtcp-mux":
|
||||
assert application_data is not None
|
||||
application_data["rtcp-mux"] = True
|
||||
|
||||
elif attribute == "ice-ufrag":
|
||||
if transport_data is not None:
|
||||
transport_data["ufrag"] = parts[0]
|
||||
|
||||
elif attribute == "ice-pwd":
|
||||
if transport_data is not None:
|
||||
transport_data["pwd"] = parts[0]
|
||||
|
||||
elif attribute in ("sendrecv", "sendonly", "recvonly", "inactive"):
|
||||
if attribute == "sendrecv":
|
||||
value = "both"
|
||||
elif attribute == "sendonly":
|
||||
value = role
|
||||
elif attribute == "recvonly":
|
||||
value = "responder" if role == "initiator" else "initiator"
|
||||
else:
|
||||
value = "none"
|
||||
|
||||
if application_data is None:
|
||||
senders = value
|
||||
else:
|
||||
application_data["senders"] = value
|
||||
|
||||
host.trigger.point(
|
||||
"XEP-0167_parse_sdp_a",
|
||||
|
|
@ -488,22 +492,15 @@ def parse_sdp(sdp: str, role: str) -> dict:
|
|||
log.debug(f"cleaning remaining private data {key!r}")
|
||||
del call_data[key]
|
||||
|
||||
# FIXME: is this really useful?
|
||||
# ICE candidates may only be specified for the first media, this
|
||||
# duplicate the candidate for the other in this case
|
||||
all_media = {k: v for k, v in call_data.items() if k in ("audio", "video")}
|
||||
if len(all_media) > 1 and not all(
|
||||
"candidates" in c["transport_data"] for c in all_media.values()
|
||||
):
|
||||
first_content = next(iter(all_media.values()))
|
||||
try:
|
||||
ice_candidates = first_content["transport_data"]["candidates"]
|
||||
except KeyError:
|
||||
ice_candidates = []
|
||||
for idx, content in enumerate(all_media.values()):
|
||||
if idx == 0:
|
||||
continue
|
||||
content["transport_data"].setdefault("candidates", ice_candidates)
|
||||
if len(all_media) > 1:
|
||||
media_with_candidates = [
|
||||
m for m in all_media.values() if "candidates" in m["transport_data"]
|
||||
]
|
||||
if len(media_with_candidates) == 1:
|
||||
log.warning(
|
||||
"SDP contains ICE candidates only for one media stream. This is non-standard behavior."
|
||||
)
|
||||
|
||||
return call_data
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue