@@ -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+
832967def 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