SDP的生成 在实际的开发过程中,大多是利用库的函数自动生成一个SDP(目前比较菜,使用库还是多一些。于是搞清楚SDP与什么有关才是目前对于我而言最重要的一环,刚刚好最近开发碰到了SRS返回的answerSDP不符合规范导致链接失败的案例就以此为分析的起点。如下为日志,去掉了一些无关部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 [2025-10-26 10:09:05.686] <WebRTCPublisher.cpp:WebRTCPublisher::init:33> Initializing WebRTC Publisher [2025-10-26 10:09:05.686] <ffmpegEncoder.cpp:ffmpegEncoder::doEncodingWork:144> Encoding [2025-10-26 10:09:05.686] <ffmpegEncoder.cpp:ffmpegEncoder::doEncodingWork:144> Encoding [2025-10-26 10:09:05.707] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::PeerConnection@50: Creating PeerConnection [2025-10-26 10:09:05.707] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::PeerConnection@77: MTU set to 1500 [2025-10-26 10:09:05.707] <libdatachannel:rtc_callback:0> rtc::impl::Certificate::Generate@445: Generating certificate (OpenSSL) [2025-10-26 10:09:05.707] <libdatachannel:rtc_callback:0> rtc::impl::Certificate::Generate@461: Generating ECDSA P-256 key pair [2025-10-26 10:09:05.709] <WebRTCPublisher.cpp:WebRTCPublisher::initializePeerConnection:60> Video track (H.264) added. [2025-10-26 10:09:05.710] <WebRTCPublisher.cpp:WebRTCPublisher::initializePeerConnection:67> Audio track (Opus) added. [2025-10-26 10:09:05.710] <libdatachannel:rtc_callback:0> rtc::PeerConnection::setLocalDescription@85: Setting local description, type=unspec [2025-10-26 10:09:05.710] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::initIceTransport@160: Starting ICE transport [2025-10-26 10:09:05.710] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::IceTransport@58: Initializing ICE transport (libjuice) [2025-10-26 10:09:05.711] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::IceTransport@113: Using STUN server "stun.l.google.com:19302" [2025-10-26 10:09:05.711] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:78: Creating agent [2025-10-26 10:09:05.712] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: ice.c:157: Created local description: ufrag="4i48", pwd="Anv07gtuGUFV8hrjIXAAtZ" [2025-10-26 10:09:05.712] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:472: Generated local SDP description: a=ice-ufrag:4i48 a=ice-pwd:Anv07gtuGUFV8hrjIXAAtZ a=ice-options:ice2,trickle [2025-10-26 10:09:05.712] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:475: Assuming controlling mode [2025-10-26 10:09:05.713] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::populateLocalDescription@1007: Adding media to local description, mid="video", removed=false [2025-10-26 10:09:05.713] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::populateLocalDescription@1007: Adding media to local description, mid="audio", removed=false [2025-10-26 10:09:05.715] <ffmpegVideoDecoder.cpp:ffmpegVideoDecoder::doDecodingPacket:104> decoding. [2025-10-26 10:09:05.716] <ffmpegVideoDecoder.cpp:ffmpegVideoDecoder::doDecodingPacket:104> decoding. [2025-10-26 10:09:05.716] <ffmpegVideoDecoder.cpp:ffmpegVideoDecoder::doDecodingPacket:72> ffmpegDecoder::startDecoding [2025-10-26 10:09:05.719] <ffmpegEncoder.cpp:ffmpegEncoder::doEncodingWork:144> Encoding [2025-10-26 10:09:05.719] <ffmpegEncoder.cpp:ffmpegEncoder::doEncodingWork:144> Encoding [2025-10-26 10:09:05.727] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::processLocalDescription@1040: Issuing local description: v=0 o=rtc 3581011564 0 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE video audio a=group:LS video audio a=msid-semantic:WMS * a=ice-options:ice2,trickle a=fingerprint:sha-256 28:DF:01:18:A0:E1:7D:57:BF:87:C0:B8:4B:03:C9:0F:95:F7:E2:BF:74:49:9B:99:91:ED:6E:6E:79:43:76:C8 m=video 9 UDP/TLS/RTP/SAVPF 96 c=IN IP4 0.0.0.0 a=mid:video a=sendonly a=rtcp-mux a=rtpmap:96 H264/90000 a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtcp-fb:96 goog-remb a=setup:actpass a=ice-ufrag:4i48 a=ice-pwd:Anv07gtuGUFV8hrjIXAAtZ m=audio 9 UDP/TLS/RTP/SAVPF 111 c=IN IP4 0.0.0.0 a=mid:audio a=sendonly a=rtcp-mux a=rtpmap:111 opus/48000/2 a=setup:actpass a=ice-ufrag:4i48 a=ice-pwd:Anv07gtuGUFV8hrjIXAAtZ [2025-10-26 10:09:05.727] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::changeSignalingState@1368: Changed signaling state to have-local-offer [2025-10-26 10:09:05.728] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::changeGatheringState@1355: Changed gathering state to in-progress [2025-10-26 10:09:05.728] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:215: Gathering candidates [2025-10-26 10:09:05.728] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1158: Changing state to gathering [2025-10-26 10:09:05.728] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn.c:65: Creating connections registry [2025-10-26 10:09:05.729] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: udp.c:166: Opening UDP socket for IPv6 family [2025-10-26 10:09:05.730] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: udp.c:96: UDP socket bound to localhost:60869 [2025-10-26 10:09:05.730] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:93: Starting connections thread [2025-10-26 10:09:05.730] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn.c:144: Creating connection [2025-10-26 10:09:05.731] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: udp.c:166: Opening UDP socket for IPv6 family [2025-10-26 10:09:05.731] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: udp.c:96: UDP socket bound to any:60870 [2025-10-26 10:09:05.731] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:299: Entering poll on 1 sockets for 0 ms [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:378: Interrupting connections thread [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:301: Leaving poll [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:251: Adding 2 local host candidates [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:285: Gathered host candidate: a=candidate:1 1 UDP 2122317823 172.24.64.1 60870 typ host [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::Candidate::parse@90: Parsing candidate: 1 1 UDP 2122317823 172.24.64.1 60870 typ host [2025-10-26 10:09:05.732] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::processLocalCandidate@1077: Issuing local candidate: a=candidate:1 1 UDP 2122317823 172.24.64.1 60870 typ host [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@152: Resolving candidate (mode=simple): 172.24.64.1 60870 [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@187: Resolved candidate: 172.24.64.1 60870 [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:285: Gathered host candidate: a=candidate:2 1 UDP 2122317567 10.0.0.10 60870 typ host [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::Candidate::parse@90: Parsing candidate: 2 1 UDP 2122317567 10.0.0.10 60870 typ host [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::processLocalCandidate@1077: Issuing local candidate: a=candidate:2 1 UDP 2122317567 10.0.0.10 60870 typ host [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@152: Resolving candidate (mode=simple): 10.0.0.10 60870 [2025-10-26 10:09:05.733] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@187: Resolved candidate: 10.0.0.10 60870 [2025-10-26 10:09:05.734] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1158: Changing state to connecting [2025-10-26 10:09:05.734] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::changeIceState@1337: Changed ICE state to checking [2025-10-26 10:09:05.734] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::changeState@1319: Changed state to connecting [2025-10-26 10:09:05.734] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:829: Bookkeeping... [2025-10-26 10:09:05.734] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:378: Interrupting connections thread [2025-10-26 10:09:05.735] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:299: Entering poll on 1 sockets for 0 ms [2025-10-26 10:09:05.735] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:297: Starting resolver thread for servers [2025-10-26 10:09:05.735] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:301: Leaving poll [2025-10-26 10:09:05.735] <WebRTCPublisher.cpp:WebRTCPublisher::startPublishing:166> Starting WebRTC Publishing [2025-10-26 10:09:05.735] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:829: Bookkeeping... [2025-10-26 10:09:05.735] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:299: Entering poll on 1 sockets for 60000 ms [2025-10-26 10:09:05.744] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:437: Using STUN server stun.l.google.com:19302 [2025-10-26 10:09:05.744] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:442: Registering STUN entry 0 for server request [2025-10-26 10:09:05.745] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2481: Updating gathering status [2025-10-26 10:09:05.745] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2486: STUN server or relay entry 0 is still pending [2025-10-26 10:09:05.745] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:378: Interrupting connections thread [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:301: Leaving poll [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:829: Bookkeeping... [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:851: STUN entry 0: Sending request to 74.125.250.129:19302 (5 retransmissions left) [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1583: Sending STUN Binding request [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:110: Writing STUN message, class=0x0, method=0x1 [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:413: Writing STUN attribute type 0x8022, length=8 [2025-10-26 10:09:05.746] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:413: Writing STUN attribute type 0x8028, length=4 [2025-10-26 10:09:05.747] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:411: Sending datagram, size=40 [2025-10-26 10:09:05.747] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:299: Entering poll on 1 sockets for 500 ms [2025-10-26 10:09:05.972] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:301: Leaving poll [2025-10-26 10:09:05.972] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:191: Receiving datagram [2025-10-26 10:09:05.972] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:765: Received datagram, size=32 [2025-10-26 10:09:05.972] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:779: Received STUN datagram from 74.125.250.129:19302 [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:535: Reading STUN message, class=0x100, method=0x1 [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:629: Reading attribute 0x20, length=8 [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:655: Reading XOR mapped address [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:1050: Reading IPv4 address [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: stun.c:552: Finished reading STUN attributes [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1273: STUN message is a response, looking for transaction ID [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2546: STUN entry 0 matching incoming transaction ID [2025-10-26 10:09:05.973] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1431: Received STUN Binding success response from server [2025-10-26 10:09:05.974] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1434: STUN server binding successful [2025-10-26 10:09:05.974] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1448: Response has mapped address [2025-10-26 10:09:05.974] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:1453: Got STUN mapped address 111.43.134.139:60870 from server [2025-10-26 10:09:05.974] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2244: Gathered reflexive candidate: a=candidate:3 1 UDP 1686109695 111.43.134.139 60870 typ srflx raddr 0.0.0.0 rport 0 [2025-10-26 10:09:05.974] <libdatachannel:rtc_callback:0> rtc::Candidate::parse@90: Parsing candidate: 3 1 UDP 1686109695 111.43.134.139 60870 typ srflx raddr 0.0.0.0 rport 0 [2025-10-26 10:09:05.975] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::processLocalCandidate@1077: Issuing local candidate: a=candidate:3 1 UDP 1686109695 111.43.134.139 60870 typ srflx raddr 0.0.0.0 rport 0 [2025-10-26 10:09:05.975] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@152: Resolving candidate (mode=simple): 111.43.134.139 60870 [2025-10-26 10:09:05.976] <libdatachannel:rtc_callback:0> rtc::Candidate::resolve@187: Resolved candidate: 111.43.134.139 60870 [2025-10-26 10:09:05.976] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2481: Updating gathering status [2025-10-26 10:09:05.976] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:2491: Candidate gathering done [2025-10-26 10:09:05.977] <libdatachannel:rtc_callback:0> rtc::impl::PeerConnection::changeGatheringState@1355: Changed gathering state to complete [2025-10-26 10:09:05.977] <WebRTCPublisher.cpp:WebRTCPublisher::initializePeerConnection::<lambda_3>::operator ():118> WebRTC ICE Gathering state changed: Complete [2025-10-26 10:09:05.977] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:191: Receiving datagram [2025-10-26 10:09:05.977] <WebRTCPublisher.cpp:WebRTCPublisher::initializePeerConnection::<lambda_3>::operator ():123> ICE Gathering complete. Sending offer to server. [2025-10-26 10:09:05.977] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:199: No more datagrams to receive [2025-10-26 10:09:05.977] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: agent.c:829: Bookkeeping... [2025-10-26 10:09:05.977] <libdatachannel:rtc_callback:0> rtc::impl::IceTransport::LogCallback@390: juice: conn_poll.c:299: Entering poll on 1 sockets for 15000 ms [2025-10-26 10:09:05.977] <WebRTCPublisher.cpp:WebRTCPublisher::initializePeerConnection::<lambda_3>::operator ():125> Local SDP Offer: v=0 o=rtc 3581011564 0 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE video audio a=group:LS video audio a=msid-semantic:WMS * a=ice-options:ice2,trickle a=fingerprint:sha-256 28:DF:01:18:A0:E1:7D:57:BF:87:C0:B8:4B:03:C9:0F:95:F7:E2:BF:74:49:9B:99:91:E m=video 60870 UDP/TLS/RTP/SAVPF 96 c=IN IP4 172.24.64.1 a=mid:video a=sendonly a=rtcp-mux a=rtpmap:96 H264/90000 a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtcp-fb:96 goog-remb a=setup:actpass a=ice-ufrag:4i48 a=ice-pwd:Anv07gtuGUFV8hrjIXAAtZ a=candidate:1 1 UDP 2122317823 172.24.64.1 60870 typ host a=candidate:2 1 UDP 2122317567 10.0.0.10 60870 typ host a=candidate:3 1 UDP 1686109695 111.43.134.139 60870 typ srflx raddr 0 a=end-of-candidates m=audio 60870 UDP/TLS/RTP/SAVPF 111 c=IN IP4 172.24.64.1 a=mid:audio a=sendonly a=rtcp-mux a=rtpmap:111 opus/48000/2 a=setup:actpass a=ice-ufrag:4i48 a=ice-pwd:Anv07gtuGUFV8hrjIXAAtZ [2025-10-26 10:09:05.996] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer:194> Sending Offer SDP to http://10.0.0.10:1985/rtc/v1/publish/ [2025-10-26 10:09:05.997] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer:197> NetworkManager exists, attempting POST request... [2025-10-26 10:09:05.997] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer:200> POST request initiated successfully. [2025-10-26 10:09:05.997] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer:220> POST request sent to signaling server. [2025-10-26 10:09:05.998] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer:244> Connections for reply signals established. [2025-10-26 10:09:06.155] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:248> onSignalingReply called! [2025-10-26 10:09:06.155] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:251> Received response from signaling server: {"code":0,"server":"vid-2904287","service":"0t0t1i71","pid":"1","sdp":"v=0\r\no=SRS/6.0.181(Hang) 108481508636688 2 IN IP4 0.0.0.0\r\ns=SRSPublishSession\r\nt=0 0\r\na=ice-lite\r\na=msid-semantic: WMS live/teststream\r\n","sessionid":"q51738jd:4i48"} [2025-10-26 10:09:06.155] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:258> Received Answer SDP from server. [2025-10-26 10:09:06.156] <libdatachannel:rtc_callback:0> rtc::PeerConnection::setRemoteDescription@184: Setting remote description: v=0 o=SRS/6.0.181(Hang) 108481508636688 0 IN IP4 127.0.0.1 s=- t=0 0 a=msid-semantic:WMS * a=ice-lite a=msid-semantic: WMS live/teststream [2025-10-26 10:09:06.159] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:270> Failed to set remote description: Remote description has no ICE user fragment [2025-10-26 10:09:06.159] <WebRTCPublisher.cpp:WebRTCPublisher::sendOfferToSignalingServer::<lambda_3>::operator ():241> QNetworkReply finished signal received. [2025-10-26 10:09:06.159] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:248> onSignalingReply called! [2025-10-26 10:09:06.159] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:251> Received response from signaling server: [2025-10-26 10:09:06.159] <WebRTCPublisher.cpp:WebRTCPublisher::onSignalingReply:276> Signaling server returned an error or invalid data: [2025-10-26 10:09:06.167] <mainwindow.cpp:MainWindow::handleError:328> Failed to set remote description: Remote description has no ICE user fragment
首先要搞清楚SDP与什么因素有关,以SRS的SDP为例,首先是以我发送的SDP为依据的,具体包括
媒体协商,会检查m= video 和 m=audio 以及自身的配置
安全协商,会根据a=fingerprint:sha—生成自己的指纹,并根据此进行DTLS握手。
ICE协商。由a=ice-ufrag 和 a=ice-pwd。它会生成自己的一对 ufrag 和 pwd 并在 Answer 中返回。它还会查看 a=candidate 列表,为后续的连通性检查做准备。
接下来是,SRS本身的配置以及环境变量。配置我就不贴了,还是比较简单的。
首先是rtc_server端口配置,一般是8080
vhost配置
candidate配置:SRS 必须告诉你的 C++ 客户端“你应该把媒体流发到哪里”。它根据 SRS_CANDIDATE=”10.0.0.10” 和它自己的内部 IP 172.17.0.2 来构建它自己的 a=candidate 列表(例如 a=candidate:… 10.0.0.10 8000 typ host),这个列表必须包含在完整的 Answer SDP 中。
SDP结构 1 2 3 4 5 6 7 8 9 10 1 v=0 2 o=- 3409821183230872764 2 IN IP4 127.0 .0 .1 3 … 4 m=audio 9 UDP /TLS /RTP /SAVPF 111 103 104 … 5 … 6 a=rtpmap :111 opus /48000 /2 7 a=rtpmap :103 ISAC /16000 8 a=rtpmap :104 ISAC /32000
标准SDP规范的规定较为简单,前三行为会话描述,对整个SDP有约束作用;第四行为媒体描述,各个媒体描述之间互不影响。在整个SDP中只能有一个会话描述,但是可以有多个媒体描述。
SDP的描述格式同样较为简单
SDP中的信息 webrtc为了实现实时的通信,对标准的SDP做了较大的调整。这里仅展示wR中的SDP内容
会话部分 1 2 3 4 v:协议版本 o:会话创建者 s:会话名 t:会话时长
(下面部分选自李超老师书籍WebRTC音视频技术)
媒体描述
媒体信息 在SDP中最重要的内容就是媒体信息。我们看一下 SDP中媒体信息的具体格式,如下所示:
1 m=<media><port>/<numbers><transport><fmt>...
其中,<media>表示媒体类型,可以是audio、video等。 <port>/<numbers>表示该媒体使用的端口号。对于WebRTC而言,由于它不使用SDP中描述的网络信息,所以该端口号对它没任何意义。<transport>表示使用的传输协议,可以是UDP、TCP等。 <fmt>表示媒体数据类型,一般为PayloadType列表,其具体含义需要使用"a=rtpmap:"属性做进一步阐述。
我们来看一个具体的例子,如代码7.2所示。从代码中可以看到, media的值为audio,表示该媒体的类型为音频;port为9,可以直接忽略,因为WebRTC不使用标准SDP中的网络信息,所以这里的端口也就失去了意义;transport为UDP/TLS/RTP/SAVFP,表示底层使用了哪些传输协议;fmtlist的值为一串从111到126的数字,每个数字代表一个PayloadType,不同的PayloadType表示媒体数据使用了不同的编解码器或编解码器参数。
上面提到的UDP/TLS/RTP/SAVFP,其含义为:传输时底层使用 UDP;在UDP之上使用了DTLS协议来交换证书;证书交换好后,媒体数据由RTP进行传输(RTP运行在UDP之上),保证传输的可靠性;媒体数据(音视频数据)的安全性是由SRTP负责的,即对RTP包中的Body部分进行加密。此外,传输时还使用RTCP的feekback机制对传输信息进行实时反馈(SAVPF),以便进行拥塞控制。 代码7.2 媒体信息
1 2 3 4 1 ... 2 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 3 ...
通过上面的介绍,我们已经清楚了SDP中的媒体信息是用来做什么的。不过媒体信息不只有上面的这些内容,它还有很多"$a=$"的属性用来对前面的信息做进一步解释,如每个PayloadType的详细参数就是由它们说明的。 音频媒体的描述前面已经介绍过了,但还有很多细节没有介绍。这些细节是无法通过一条" $\mathrm{m}=$"行就能够描述清楚的,必须通过 "$a=r t p m a p$"对其做进一步解释才行。如代码7.3所示,在这段代码中,使用大量的" $\mathrm{a}=\mathrm{rtpmap”}$ 属性对" $\mathrm{m}=$"行做进一步阐释。
代码7.3 音频媒体示例
1 2 3 4 5 6 7 8 9 m = audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 ... ... a=rtpmap :111 opus /48000/2 a=rtcp -fb:111 transport -cc a=fmtp :111 minptime =10; useinbandfec =1 a=rtpmap :103 ISAC /16000 a=rtpmap :104 ISAC /32000 a=rtpmap :9 G722 /8000 ...
这段代码中的第 1 行代码是对音频媒体的描述;第 $3 、 6 、 7 、 8$ 行代码使用"a=rtpmap"解释了PayloadType使用的编解码器及其参数是什么;第 5 行代码"$a=f m t p$"属性指定了PayloadType的数据格式,即音频帧最小 10 ms —帧,使用带内FEC。
在WebRTC的SDP中,"a=rtpmap""a=fmtp"属性随处可见。无论是音频媒体中,还是视频媒体中,都使用它们对媒体做进一步的解释。
"a=rtpmap"属性
rtpmap(rtp map),通过字面含义可以知道它是一张 PayloadType与编码器的映射表,每个PayloadType都对应一个编码器。其格式定义在RFC4566中,如下所示: a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encodingparameters>]
通过上面rtpmap的格式,可以很容易理解代码7.3中第3行代码的含义:Payload Type值为 111 的编码器是Opus,其时钟频率(采样率)为48000,音频通道数为2。同理,PayloadType值为103的编码器是ISAC,采样率为 16000 ;PayloadType值为 104 的编码器也是 ISAC,只不过其采样率变成了 32000 ;PayloadType值为 9 的编码器是G722,采样率是 $8000 . . . . .$.
" $\mathrm{a}=\mathrm{fmtp}$"属性 fmtp(format parameters)用于指定媒体数据格式。 "$a=f m t p$"属性的格式与rtpmap一样也是定义在RFC4566的第6节中,如下所示: a=fmtp:<format><format specific parameters>
现在再来看一下代码7.3中的第5行代码,它描述了PayloadType值为 111 的数据(Opus数据):以 10 ms 长的音频数据为一帧,并且数据是经FEC编码的。其中,"usein bandfec $=1$"是WebRTC针对 Opus增加的fmtp值。如果你想了解这些细节,可以看一下相关的草案。
与音频媒体信息相比,视频媒体信息要复杂一些,在SDP中视频相关的描述如代码7.4所示。
代码7.4 视频媒体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 m=video 9 UDP/TLS/RTP/SAVPF 96 ... 102 121 124 ... a=mid:1 a=rtpmap :96 VP8 /90000 ... a=rtpmap :97 rtx /90000 a=fmtp :97 apt =96 ... a=rtpmap :102 H264 /90000 ... a=fmtp :102 level -asymmetry -allowed =1; packetization - mode =1; profile -level -id =42001f 13 a=rtpmap :121 rtx /90000 14 a=fmtp :121 apt =102 15 ... 16 a=rtpmap :124 red /90000 17 a=rtpmap :119 rtx /90000 18 a=fmtp :119 apt =124 19 ...
其中,第 1 行代码为视频的" $\mathrm{m}=$"行,其与音频的" $\mathrm{m}=$"行类似,区别在于两者的媒体类型不同:一个是"video",另一个则是 "audio"。此外,第1行中的PayloadType列表也发生了变化,这个很好理解,视频媒体使用的编码器本就与音频媒体使用的不同。第3行代码表明视频媒体的ID编号为1,而音频媒体的ID编号为0。如果有更多的媒体,编号会一直累加。第 $5 \sim 18$ 行代码是对不同 PayloadType的解释,下面看一下它们是如何解释PayloadType的吧。
第 5 行代码,PT(PayloadType)值为 96 表示媒体数据使用的编码器是VP8,其时钟频率为 90000 。又因为其排在" $\mathrm{m}=$"行PT列表的第一位,所以它还是视频的默认编码器。同理,代码第 10 行,PT值为102表示媒体数据使用的是H264编码器,时钟频率也是 90000 。
第 7 行代码,PT值为 97 表示的含义与之前 PT值为 96 的情况有所不同,rtx表示的不再是编码器,而是丢包重传。要想弄明白第 7 行代码的含义,必须与第 8 行代码结合着一起看。在第 8 行代码中, apt(associated payload type)的值为 96 ,说明 96 与 97 是关联在一起的,PT=97是PT=96的补充。因此第7行代码的含义是:当 WebRTC使用的媒体数据类型(PayloadType)为96时,如果出现丢包需要重传,重传数据包的PayloadType为97。同理,第13~14行代码指明121是PT=102重传包的PayloadType。
第16~18行代码较为特殊,要想了解这三行代码的含义,你还需要了解一些额外知识:一是red,它是一种在WebRTC中使用的FEC算法,用于防止丢包;二是red编码流程,默认情况下WebRTC会将 VP8/H264等编码器编码后的数据再交由red模块编码,生成带一些冗余信息的数据包,这样当传输中某个包丢了,就可以通过其他包将其恢复回来,而不用重传丢失的包。了解了上面这些内容后,第 $20 \sim 22$行代码的含义应该就清楚了,即PT值为 124 表示需要使用red对之前编码好的数据再进行 red 处理, 119 是 PT $=124$ 重传数据包的 PayloadType。如果用Wireshark等抓包工具抓取WebRTC媒体数据包时会发现它们都是red包,而在red包里装的是VP8/H264编码的数据。
再看一下与 H 264 相关的 fmtp 内 容。第 12 行代码,level- asymmetry-allowed $=1$ 指明通信双方使用的 H264Level是否要保持一致:0,必须一致;1,可以不一致。packetization mode指明经 H264编码后的视频数据如何打包,其打包模式有三种:0,单包;1,非交错包;2,交错包。三种打包模式中,模式0和模式1用在低延时的实时通信领域。其中模式 0 的含义是每个包就是一帧视频数据;模式1的含义是可以将视频帧拆成多个顺序的RTP包发送,接收端收到数据包后,再按顺序将其还原。profile-level-id由三部分组成,即 profile_idc、profile_iop以及level_idc,每个组成部分占8位,因此可以推测出profile_idc $=42$ 、profile_iop $=00$ 、level_idc $=1$ f。关于这几个值的具体含义,如果读者感兴趣,可以自行查看H264规范手册。
以上分析将SDP中视频媒体信息相关的内容及其含义讲解清楚了。音视频媒体信息是SDP中最为重要的内容,读者一定要牢牢掌握。
另外一个媒体描述是SSRC,它是媒体源的唯一标识。需要注意的是,虽然原则上要求每一路媒体流都只有一个唯一的SSRC来标识它。但是我们可以使用
1 a = ssrc -grou:FID XXXXX MMMMM
不同的ssrc标识符来区分真正的视频流,以及重传的视频流。
SDP的版本 1 标准SDP->PlanB->UnifiedPlan
PlanB和Unified的最大的区别是,前者只有两个媒体描述,而如果要穿上多路的音视频流,那么这个时候要使用SSRC来进行区分,而在后这中可以有多个媒体描述,因此对于多路视频的情况只需要拆分成多个媒体描述(“m= ”)即可
RTP扩展头 通过使用“a=exemap”扩展头,在原有的UDP基础上完完成扩展,进行SDP传输
服务质量 “a=rtcp-fb”,需要注意的是,这个字段即可以表示RTCP中专门反馈消息的一类消息。二是设置终端支持哪些feedback消息,通过设置编码器,拥塞算法等参数,影响服务质量。