open-standards/wai/live-conformance/sdp_caps_verify.py
Transaction Science b19e078cc3 wai: capability attributes in a WHEP offer, so the gateway forwards each player the rendition its sink dispatches first
live-ingest §9.5: a WAI-aware WHEP player writes, per media section of its offer, the WAI
capabilities its sink dispatches, in its dispatch order, as SDP media capabilities
(RFC 6871) under capability negotiation (RFC 5939): a=csup:med-v0, then for each capability
§5.1 carries over WebRTC (wai.video.avc, vp8, vp9, av1, wai.audio.opus.packet) that the
section offers, an a=rmcap, an a=mfcap of its fmtp, and an a=pcfg whose number gives the
order. The egress gateway reads them back, refuses with 422 an offer whose capability
attributes do not parse or do not hold together (caps_malformed, caps_number_reused,
caps_config_reused, caps_unknown_capability, caps_payload_type, caps_option_missing,
caps_unsupported, caps_too_many; §9.5 and §14), forwards for each section the rendition the
first advertised capability names at or below the grant's max_layer, answered with a=acfg,
and the floor where nothing advertised is published. §17 records that joule-moq-ingest does
not read them yet.

wai::sdp_caps: read, Offer::advertised, pick and Pick::acfg, write. Its tests replay
live-conformance/sdp-caps/vectors.json (offers read and refused with every reason, picks,
offers written byte for byte and read back), write and read back every order of the video
capabilities, and hold §9.5's table and reasons and §14's to the module.
live-conformance/sdp_caps_verify.py, written from §9.5 and the RFCs' grammars and sharing no
code with wai-rs, recomputes every case and requires every mutated copy to fail.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:08:15 -05:00

492 lines
19 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the SDP capability attributes of a WHEP offer
(extensions/live-ingest.md §9.5).
Written from §9.5's text and the grammars it cites (RFC 5939 §3.3 and §3.5, RFC 6871 §3.3
and §3.4); it shares no code with wai-rs's sdp_caps, which the corpus is checked against too.
It reads from §9.5 the table of the capabilities a gateway carries over WebRTC, and from §14
the reasons. For every case of sdp-caps/vectors.json it recomputes:
- `reads`: the offer read or refused, with the refusal's reason; for an offer read, each media
section's advertised WAI capabilities in order, each with its configuration, media
capability and payload type;
- `picks`: the rendition a gateway forwards for a section, or the floor, and the answer's
`a=acfg` line;
- `writes`: the offer with a sink's capabilities written into it, byte for byte, the
capabilities left out, and that reading it back gives the capabilities written, in order.
It requires every reason of §9.5 to appear in the corpus and §14 to list each. Then it
mutates copies of the corpus, one defect each, and requires every copy to fail.
python3 -I sdp_caps_verify.py <live-conformance/sdp-caps>
Exit status 0 only if every check holds and every mutated copy fails. Python 3.8 or later;
standard library only.
"""
import copy
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
WAI = os.path.normpath(os.path.join(HERE, ".."))
def read_text(p):
with open(p, encoding="utf-8") as f:
return f.read()
LI = read_text(os.path.join(WAI, "extensions", "live-ingest.md"))
def section(text, heading, stop):
i = text.index(heading)
return text[i:text.index(stop, i + len(heading))]
def ticks(s):
return re.findall(r"`([^`]*)`", s)
def rtp_rows():
"""§9.5's table: capability, encoding name, clock rate, encoding parameters."""
rows = []
for line in section(LI, "### 9.5 ", "## 10.").splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if line.startswith("| `wai.") and len(cells) == 4:
params = ticks(cells[3])
rows.append((ticks(cells[0])[0], ticks(cells[1])[0], int(ticks(cells[2])[0]), params[0] if params else None))
assert rows, "§9.5 has no table"
return rows
def reasons_95():
out = []
for line in section(LI, "### 9.5 ", "## 10.").splitlines():
if line.startswith("| `caps_"):
out.append(ticks(line)[0])
return out
def reasons_14():
out = set()
for line in section(LI, "## 14. Reasons", "## 15.").splitlines():
if line.startswith("| `"):
out.update(ticks(line.split("|")[1]))
return out
ROWS = rtp_rows()
REASONS = reasons_95()
MAX_CAPS = 1024
class Refusal(Exception):
def __init__(self, reason, detail):
Exception.__init__(self, "%s: %s" % (reason, detail))
self.reason = reason
TOKEN = set(b"!#$%&'*+-.^_`{|}~") | set(range(0x30, 0x3A)) | set(range(0x41, 0x5B)) | set(range(0x61, 0x7B))
def is_token(s):
return bool(s) and all(c in TOKEN for c in s.encode("latin-1", "replace"))
def num(s, nonzero=False):
if not s or len(s) > 10 or not s.isdigit() or not all(c in "0123456789" for c in s):
return None
if nonzero and s[0] == "0":
return None
v = int(s)
return v if 1 <= v <= 2 ** 31 - 1 else None
def cap_list(s):
out = []
for el in s.split(","):
if "-" in el:
a, b = el.split("-", 1)
a, b = num(a, True), num(b, True)
if a is None or b is None or a >= b:
raise Refusal("caps_malformed", "range " + el)
if b - a >= MAX_CAPS:
raise Refusal("caps_too_many", "range " + el)
out.extend(range(a, b + 1))
else:
n = num(el, True)
if n is None:
raise Refusal("caps_malformed", "number " + el)
out.append(n)
return out
def wsp(s):
m = re.match(r"([^ \t]*)[ \t]+(.+)$", s, re.S)
return (m.group(1), m.group(2)) if m and m.group(1) else None
def att_config(val):
"""RFC 5939 attribute-config-list after its "a="; None when it is off the grammar."""
dels = ("-m", "-s", "-ms")
if ":" in val:
d, lists = val.split(":", 1)
if d not in dels:
return None
elif val in dels:
return []
else:
lists = val
out = []
for alt in lists.split("|"):
m = re.fullmatch(r"(\d+(?:,\d+)*)?(?:(?(1),)\[(\d+(?:,\d+)*)\])?", alt)
if not m or not alt:
return None
for g in (m.group(1), m.group(2)):
for n in (g.split(",") if g else []):
if num(n) is None:
return None
out.append(num(n))
return out
def pcfg(number, rest):
cfg = {"number": number, "media": [], "pt": [], "skip": False}
a_refs, t_refs, seen = [], [], set()
for item in [x for x in re.split(r"[ \t]+", rest) if x]:
plus = item.startswith("+")
body = item[1:] if plus else item
if "=" not in body:
raise Refusal("caps_malformed", item)
key, val = body.split("=", 1)
if key in ("a", "t", "m", "pt"):
if key in seen:
raise Refusal("caps_malformed", "a second " + key)
seen.add(key)
if key == "a" and not plus:
got = att_config(val)
if got is None:
raise Refusal("caps_malformed", item)
a_refs += got
cfg["skip"] = True
elif key == "t" and not plus:
for n in val.split("|"):
if num(n) is None:
raise Refusal("caps_malformed", item)
t_refs.append(num(n))
cfg["skip"] = True
elif key == "m":
cfg["media"] = [cap_list(alt) for alt in val.split("|")]
elif key == "pt":
for mp in val.split(","):
if ":" not in mp:
raise Refusal("caps_malformed", item)
m, p = mp.split(":", 1)
m, pv = num(m, True), num(p, True)
if m is None or pv is None or len(p) > 3:
raise Refusal("caps_malformed", item)
if pv > 127:
raise Refusal("caps_payload_type", item)
if any(q == pv or c == m for c, q in cfg["pt"]):
raise Refusal("caps_payload_type", item)
cfg["pt"].append((m, pv))
else:
if not key or not all(c.isalnum() and c.isascii() for c in key) or not val or not all(0x21 <= ord(c) <= 0x7E for c in val):
raise Refusal("caps_malformed", item)
if plus:
cfg["skip"] = True
return cfg, a_refs, t_refs
def sdp_lines(sdp):
return [l[:-1] if l.endswith("\r") else l for l in sdp.split("\n") if l not in ("", "\r")]
def read(sdp):
options, required, caps, mf, acap, tcap, sections, cfgnums, pending, creq_at = set(), [], {}, [], {}, {}, [], set(), [], set()
for line in sdp_lines(sdp):
if line.startswith("m="):
sections.append({"media": line[2:].split(" ")[0], "configs": []})
continue
if not line.startswith("a="):
continue
attr = line[2:]
name, value = attr.split(":", 1) if ":" in attr else (attr, "")
here = len(sections) - 1 if sections else None
if name in ("csup", "creq"):
tags = value.split(",")
if not all(is_token(t) for t in tags):
raise Refusal("caps_malformed", value)
if name == "creq":
if here in creq_at:
raise Refusal("caps_malformed", "a second creq at one level")
creq_at.add(here)
required += tags
options.update(tags)
elif name in ("rmcap", "omcap"):
sp = wsp(value)
if not sp:
raise Refusal("caps_malformed", attr)
nums = cap_list(sp[0])
if name == "rmcap":
parts = sp[1].split("/")
if len(parts) not in (2, 3) or not is_token(parts[0]) or (len(parts) == 3 and not is_token(parts[2])):
raise Refusal("caps_malformed", attr)
rate = num(parts[1], True)
if rate is None:
raise Refusal("caps_malformed", attr)
entry = (True, parts[0], rate, parts[2] if len(parts) == 3 else None)
else:
if not is_token(sp[1]):
raise Refusal("caps_malformed", attr)
entry = (False, sp[1], None, None)
for n in nums:
if n in caps:
raise Refusal("caps_number_reused", str(n))
caps[n] = {"rtp": entry[0], "name": entry[1], "rate": entry[2], "params": entry[3], "level": here, "fmtp": []}
if len(caps) > MAX_CAPS:
raise Refusal("caps_too_many", "count")
elif name == "mfcap":
sp = wsp(value)
if not sp:
raise Refusal("caps_malformed", attr)
mf.append((cap_list(sp[0]), sp[1]))
elif name in ("acap", "tcap"):
sp = wsp(value)
if not sp or num(sp[0]) is None:
raise Refusal("caps_malformed", attr)
ok = is_token(sp[1].split(":")[0]) if name == "acap" else all(is_token(t) for p in re.split(r"[ \t]+", sp[1]) if p for t in p.split("/"))
if not ok:
raise Refusal("caps_malformed", attr)
table = acap if name == "acap" else tcap
if num(sp[0]) in table:
raise Refusal("caps_number_reused", attr)
table[num(sp[0])] = here
elif name == "pcfg":
if here is None:
raise Refusal("caps_malformed", "pcfg at the session level")
sp = wsp(value)
n_s, rest = sp if sp else (value, "")
n = num(n_s)
if n is None:
raise Refusal("caps_malformed", attr)
if n in cfgnums:
raise Refusal("caps_config_reused", str(n))
cfgnums.add(n)
cfg, ar, tr = pcfg(n, rest)
pending.append((here, cfg, ar, tr))
elif name in ("sescap", "lcfg"):
raise Refusal("caps_unsupported", name)
if caps and "med-v0" not in options:
raise Refusal("caps_option_missing", "no med-v0")
for t in required:
if t not in ("cap-v0", "med-v0"):
raise Refusal("caps_unsupported", t)
def vis(level, sec):
return level is None or level == sec
for nums, params in mf:
for n in nums:
if n not in caps:
raise Refusal("caps_unknown_capability", "mfcap %d" % n)
caps[n]["fmtp"].append(params)
for sec, cfg, ar, tr in pending:
for alt in cfg["media"]:
for n in alt:
if n not in caps or not vis(caps[n]["level"], sec):
raise Refusal("caps_unknown_capability", "m %d" % n)
if caps[n]["rtp"] and n not in [c for c, _ in cfg["pt"]]:
raise Refusal("caps_payload_type", "no pt for %d" % n)
for c, _ in cfg["pt"]:
if c not in caps or not vis(caps[c]["level"], sec):
raise Refusal("caps_unknown_capability", "pt %d" % c)
for n in ar:
if n not in acap or not vis(acap[n], sec):
raise Refusal("caps_unknown_capability", "a %d" % n)
for n in tr:
if n not in tcap or not vis(tcap[n], sec):
raise Refusal("caps_unknown_capability", "t %d" % n)
sections[sec]["configs"].append(cfg)
return {"caps": caps, "sections": sections}
def wai_cap(c):
if not c["rtp"]:
return None
for cap, enc, rate, params in ROWS:
if enc.lower() == c["name"].lower() and rate == c["rate"] and params == c["params"]:
return cap
return None
def advertised(offer, sec):
out = []
for cfg in sorted((c for c in offer["sections"][sec]["configs"] if not c["skip"]), key=lambda c: c["number"]):
for alt in cfg["media"]:
for n in alt:
cap = wai_cap(offer["caps"][n])
if cap is None or any(a["capability"] == cap for a in out):
continue
pt = [p for c, p in cfg["pt"] if c == n][0]
out.append({"capability": cap, "config": cfg["number"], "cap": n, "payload_type": pt})
return out
def pick(adv, renditions, max_layer, floor):
for a in adv:
rs = [r for r in renditions if r["layer"] <= max_layer and r["capability"] == a["capability"]]
if rs:
r = min(rs, key=lambda r: r["layer"])
return {"layer": r["layer"], "capability": r["capability"], "config": a["config"], "cap": a["cap"], "payload_type": a["payload_type"],
"acfg": "a=acfg:%d m=%d pt=%d:%d" % (a["config"], a["cap"], a["cap"], a["payload_type"])}
return {"layer": floor, "floor": True, "acfg": None}
CAPS_ATTRS = ("a=csup:", "a=creq:", "a=rmcap:", "a=omcap:", "a=mfcap:", "a=pcfg:", "a=acap:", "a=tcap:")
def kind(cap):
return "audio" if cap.startswith("wai.audio.") else "video"
def write(offer, wanted):
eol = "\r\n" if "\r\n" in offer else "\n"
ls = sdp_lines(offer)
if any(l.startswith(CAPS_ATTRS) for l in ls):
return None, "caps_present"
ms = [i for i, l in enumerate(ls) if l.startswith("m=")]
if not ms:
return None, "no_media"
out = ls[:ms[0]] + ["a=csup:med-v0"]
used, n, c = set(), 1, 1
for k, st in enumerate(ms):
sec = ls[st:ms[k + 1] if k + 1 < len(ms) else len(ls)]
out += sec
parts = sec[0][2:].split(" ")
for cap in wanted:
row = [r for r in ROWS if r[0] == cap]
if not row or kind(cap) != parts[0]:
continue
_, enc, rate, params = row[0]
hit = None
for pt in parts[3:]:
rm = [l[len("a=rtpmap:%s " % pt):] for l in sec if l.startswith("a=rtpmap:%s " % pt)]
if rm:
p = rm[0].split("/")
if len(p) >= 2 and p[0].lower() == enc.lower() and p[1] == str(rate) and (p[2] if len(p) > 2 else None) == params:
hit = pt
break
if hit is None:
continue
used.add(cap)
out.append("a=rmcap:%d %s/%d%s" % (n, enc, rate, "/" + params if params else ""))
fm = [l[len("a=fmtp:%s " % hit):] for l in sec if l.startswith("a=fmtp:%s " % hit)]
if fm:
out.append("a=mfcap:%d %s" % (n, fm[0]))
out.append("a=pcfg:%d m=%d pt=%d:%s" % (c, n, n, hit))
n += 1
c += 1
return eol.join(out) + eol, [w for w in wanted if w not in used]
def verify(doc):
fails, seen = [], set()
for c in doc["reads"]:
try:
o = read(c["offer"])
got = {"outcome": "read", "sections": [{"media": s["media"], "advertised": advertised(o, i)} for i, s in enumerate(o["sections"])]}
except Refusal as e:
got = {"outcome": "refused", "reason": e.reason}
seen.add(e.reason)
want = {k: c[k] for k in ("outcome", "sections", "reason") if k in c}
if got != want:
fails.append("read %s: %s" % (c["name"], json.dumps(got)[:300]))
for c in doc["picks"]:
try:
o = read(c["offer"])
except Refusal as e:
fails.append("pick %s: the offer is refused (%s)" % (c["name"], e))
continue
got = pick(advertised(o, c["section"]), c["renditions"], c["max_layer"], c["floor_layer"])
if got != c["pick"]:
fails.append("pick %s: %s" % (c["name"], json.dumps(got)))
for c in doc["writes"]:
out, left = write(c["offer"], c["capabilities"])
if out is None:
if c.get("refused") != left:
fails.append("write %s: refused %s" % (c["name"], left))
continue
if out != c["written"] or left != c["left_out"]:
fails.append("write %s: %s" % (c["name"], json.dumps([out, left])[:300]))
continue
o = read(out)
for i, s in enumerate(o["sections"]):
back = [a["capability"] for a in advertised(o, i)]
if back != [w for w in c["capabilities"] if w not in left and kind(w) == s["media"]]:
fails.append("write %s: section %d read back as %s" % (c["name"], i, back))
for r in REASONS:
if r not in seen:
fails.append("nothing in the corpus gives %s" % r)
return fails
def mutations(doc):
def find(d, fam, name):
return next(c for c in d[fam] if c["name"] == name)
out = []
for c in doc["reads"]:
if c["outcome"] == "refused":
out.append(("%s read as another refusal" % c["name"], lambda d, n=c["name"]: find(d, "reads", n).update(reason="caps_other")))
else:
out.append(("%s's sections reordered" % c["name"], lambda d, n=c["name"]: find(d, "reads", n)["sections"].reverse() if len(find(d, "reads", n)["sections"]) > 1 else find(d, "reads", n).update(outcome="refused", reason="caps_malformed")))
if any(s["advertised"] for s in c["sections"]):
out.append(("%s's first advertised capability dropped" % c["name"], lambda d, n=c["name"]: [s["advertised"].pop(0) for s in find(d, "reads", n)["sections"] if s["advertised"]][:1]))
for c in doc["picks"]:
out.append(("%s's layer moved" % c["name"], lambda d, n=c["name"]: find(d, "picks", n)["pick"].update(layer=find(d, "picks", n)["pick"]["layer"] + 1)))
for c in doc["writes"]:
if c.get("written"):
out.append(("%s's text changed" % c["name"], lambda d, n=c["name"]: find(d, "writes", n).update(written=find(d, "writes", n)["written"].replace("a=pcfg:1 ", "a=pcfg:2 "))))
kept = [w for w in c["capabilities"] if w not in c["left_out"]]
if any(sum(1 for w in kept if kind(w) == k) > 1 for k in ("audio", "video")):
out.append(("%s's order swapped" % c["name"], lambda d, n=c["name"]: find(d, "writes", n)["capabilities"].reverse()))
return out
def main():
if len(sys.argv) != 2:
print(__doc__)
return 2
with open(os.path.join(sys.argv[1], "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
bad = 0
missing = [r for r in REASONS if r not in reasons_14()]
if missing or not REASONS:
print("FAIL §14 does not list %s" % missing)
bad += 1
fails = verify(doc)
for f in fails:
print("FAIL " + f)
n = sum(len(doc[k]) for k in ("reads", "picks", "writes"))
print("%s every case gets the outcome the corpus states (%d cases; every reason of §9.5 given)" % ("PASS" if not fails else "FAIL", n))
bad += bool(fails)
muts = mutations(doc)
caught = 0
for name, change in muts:
d2 = copy.deepcopy(doc)
change(d2)
if verify(d2):
caught += 1
else:
print("FAIL the mutation '%s' is not caught" % name)
print("%s %d of %d mutated copies fail" % ("PASS" if caught == len(muts) else "FAIL", caught, len(muts)))
bad += caught != len(muts)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())