Skip to content

Commit aeedb68

Browse files
committed
fix(gstreamer): Make Gst 1.18 audio SDP Chrome-compatible
1 parent 54f0d54 commit aeedb68

1 file changed

Lines changed: 161 additions & 11 deletions

File tree

publish.py

Lines changed: 161 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,141 @@ def fix_audio_rtcp_fb_for_gstreamer(sdp):
829829

830830
return '\r\n'.join(result_lines)
831831

832+
def strip_audio_rtx_from_sdp(sdp: str) -> str:
833+
"""Strip RTX from the audio m= section.
834+
835+
Older GStreamer/webrtcbin builds may offer RTX on the audio m-line
836+
(rtx/48000 + ssrc-group:FID). Chrome/WebRTC implementations can reject such
837+
offers with errors like "Failed to add remote stream ssrc ... (audio)".
838+
"""
839+
if not sdp or "m=audio" not in sdp:
840+
return sdp
841+
842+
ends_with_crlf = sdp.endswith("\r\n")
843+
lines = sdp.split("\r\n")
844+
if ends_with_crlf and lines and lines[-1] == "":
845+
lines = lines[:-1]
846+
847+
session_lines: List[str] = []
848+
media_sections: List[List[str]] = []
849+
current_section: Optional[List[str]] = None
850+
851+
for line in lines:
852+
if line.startswith("m="):
853+
if current_section is None:
854+
current_section = [line]
855+
else:
856+
media_sections.append(current_section)
857+
current_section = [line]
858+
else:
859+
if current_section is None:
860+
session_lines.append(line)
861+
else:
862+
current_section.append(line)
863+
864+
if current_section is not None:
865+
media_sections.append(current_section)
866+
867+
updated_sections: List[List[str]] = []
868+
changed = False
869+
870+
for section in media_sections:
871+
if not section:
872+
updated_sections.append(section)
873+
continue
874+
875+
m_line = section[0]
876+
if not m_line.startswith("m=audio"):
877+
updated_sections.append(section)
878+
continue
879+
880+
rtx_pts: Set[str] = set()
881+
rtx_ssrcs: Set[str] = set()
882+
883+
for entry in section[1:]:
884+
if entry.startswith("a=rtpmap:") and " rtx/" in entry.lower():
885+
try:
886+
pt = entry.split(":", 1)[1].split(None, 1)[0].strip()
887+
except Exception:
888+
continue
889+
if pt:
890+
rtx_pts.add(pt)
891+
if entry.startswith("a=ssrc-group:FID"):
892+
# Format: a=ssrc-group:FID <primary-ssrc> <rtx-ssrc> ...
893+
parts = entry.split()
894+
if len(parts) >= 3:
895+
rtx_ssrcs.update(parts[2:])
896+
897+
if not rtx_pts and not rtx_ssrcs:
898+
updated_sections.append(section)
899+
continue
900+
901+
m_parts = m_line.split()
902+
if len(m_parts) >= 4 and rtx_pts:
903+
payloads = [pt for pt in m_parts[3:] if pt not in rtx_pts]
904+
if payloads != m_parts[3:]:
905+
m_line = " ".join(m_parts[:3] + payloads)
906+
changed = True
907+
908+
filtered: List[str] = [m_line]
909+
for entry in section[1:]:
910+
entry_stripped = entry.strip()
911+
912+
if rtx_pts and entry_stripped.startswith("a=rtpmap:"):
913+
try:
914+
pt = entry_stripped.split(":", 1)[1].split(None, 1)[0].strip()
915+
except Exception:
916+
pt = ""
917+
if pt in rtx_pts:
918+
changed = True
919+
continue
920+
921+
if rtx_pts and entry_stripped.startswith("a=fmtp:"):
922+
try:
923+
pt = entry_stripped.split(":", 1)[1].split(None, 1)[0].strip()
924+
except Exception:
925+
pt = ""
926+
if pt in rtx_pts:
927+
changed = True
928+
continue
929+
930+
if rtx_pts and entry_stripped.startswith("a=rtcp-fb:"):
931+
try:
932+
pt = entry_stripped.split(":", 1)[1].split(None, 1)[0].strip()
933+
except Exception:
934+
pt = ""
935+
if pt in rtx_pts:
936+
changed = True
937+
continue
938+
939+
if entry_stripped.startswith("a=ssrc-group:FID"):
940+
# Drop RTX SSRC groups from audio.
941+
changed = True
942+
continue
943+
944+
if rtx_ssrcs and entry_stripped.startswith("a=ssrc:"):
945+
match = re.match(r"a=ssrc:(\d+)\b", entry_stripped)
946+
if match and match.group(1) in rtx_ssrcs:
947+
changed = True
948+
continue
949+
950+
filtered.append(entry)
951+
952+
updated_sections.append(filtered)
953+
954+
if not changed:
955+
return sdp
956+
957+
output_lines: List[str] = []
958+
output_lines.extend(session_lines)
959+
for section in updated_sections:
960+
output_lines.extend(section)
961+
962+
out = "\r\n".join(output_lines)
963+
if ends_with_crlf:
964+
out += "\r\n"
965+
return out
966+
832967
def generateHash(input_str, length=None):
833968
input_bytes = input_str.encode('utf-8')
834969
sha256_hash = hashlib.sha256(input_bytes).digest()
@@ -6910,11 +7045,9 @@ def on_offer_created(promise, _, __):
69107045
promise.wait()
69117046
reply = promise.get_reply()
69127047
offer = reply.get_value('offer')
6913-
promise = Gst.Promise.new()
6914-
client['webrtc'].emit('set-local-description', offer, promise)
6915-
promise.interrupt()
69167048
printc("📤 Sending connection offer...", "77F")
6917-
text = offer.sdp.as_text()
7049+
original_text = offer.sdp.as_text()
7050+
text = original_text
69187051
if ("96 96 96 96 96" in text):
69197052
printc("Patching SDP due to Gstreamer webRTC bug - none-unique line values","A6F")
69207053
text = text.replace(" 96 96 96 96 96", " 96 96 97 98 96")
@@ -6931,15 +7064,17 @@ def on_offer_created(promise, _, __):
69317064
printc("Patching SDP due to Gstreamer webRTC bug - audio-only issue", "A6F") # just chrome doesn't handle this
69327065
text = replace_ssrc_and_cleanup_sdp(text)
69337066

6934-
# GStreamer 1.18 has multiple audio SDP bugs that Chrome rejects:
6935-
# - Invalid SSRCs, rtcp-fb attributes, bundle issues
6936-
# Strip audio from SDP entirely for GStreamer < 1.20 as a workaround
7067+
# Fix audio SDP issues for GStreamer < 1.20 (1.18 has known SDP bugs Chrome rejects)
69377068
gst_ver = Gst.version()
6938-
if gst_ver.major == 1 and gst_ver.minor < 20:
7069+
if not self.noaudio and gst_ver.major == 1 and gst_ver.minor < 20:
7070+
if 'm=audio' in text:
7071+
printc("Patching audio SDP for GStreamer 1.18 compatibility", "A6F")
7072+
text = fix_audio_ssrc_for_ohttp_gstreamer(text)
7073+
text = fix_audio_rtcp_fb_for_gstreamer(text)
7074+
text = strip_audio_rtx_from_sdp(text)
7075+
elif self.noaudio and gst_ver.major == 1 and gst_ver.minor < 20:
69397076
if 'm=audio' in text:
6940-
if not self.noaudio:
6941-
printc("⚠️ Stripping audio from SDP (GStreamer 1.18 audio bugs)", "FA0")
6942-
printc(" └─ Upgrade to GStreamer 1.20+ for audio support", "FA0")
7077+
printc("Stripping phantom audio section from SDP (GStreamer 1.18 bug)", "A6F")
69437078
text = strip_audio_from_sdp(text)
69447079

69457080
text = self._ensure_primary_video_codec_in_sdp(text)
@@ -6955,6 +7090,21 @@ def on_offer_created(promise, _, __):
69557090
if self.view:
69567091
text = self._apply_bitrate_constraints_to_sdp(text, context="outgoing offer")
69577092

7093+
offer_to_set = offer
7094+
if text != original_text:
7095+
try:
7096+
res, sdpmsg = GstSdp.SDPMessage.new()
7097+
GstSdp.sdp_message_parse_buffer(bytes(text.encode()), sdpmsg)
7098+
offer_to_set = GstWebRTC.WebRTCSessionDescription.new(GstWebRTC.WebRTCSDPType.OFFER, sdpmsg)
7099+
except Exception as exc:
7100+
printwarn(f"Failed to rebuild modified offer SDP: {exc}")
7101+
text = original_text
7102+
offer_to_set = offer
7103+
7104+
promise = Gst.Promise.new()
7105+
client['webrtc'].emit('set-local-description', offer_to_set, promise)
7106+
promise.interrupt()
7107+
69587108
msg = {'description': {'type': 'offer', 'sdp': text}, 'UUID': client['UUID'], 'session': client['session'], 'streamID':self.stream_id+self.hashcode}
69597109
self.sendMessage(msg)
69607110

0 commit comments

Comments
 (0)