webrtc 有關 SDP 部分的解析流程分析
----------------------------------------------------------------------------------------------------------------------------------------
一分鐘快速搭建 rtmpd 伺服器: https://blog.csdn.net/freeabc/article/details/102880984
軟體下載地址: http://www.qiyicc.com/download/rtmpd.rar
github 地址:https://github.com/superconvert/smart_rtmpd
-----------------------------------------------------------------------------------------------------------------------------------------
webrtc 有關 SDP 部分的解析流程分析
在這個例子裡,我們以 android 端為例進行說明整個流程,但整個流程都發生在 JNI 層,別的基本一樣。
1.
./sdk/android/src/jni/pc/session_description.cc
std::unique_ptr<SessionDescriptionInterface> JavaToNativeSessionDescription(
JNIEnv* jni,
const JavaRef<jobject>& j_sdp) {
std::string std_type = JavaToStdString(
jni, Java_SessionDescription_getTypeInCanonicalForm(jni, j_sdp));
std::string std_description =
JavaToStdString(jni, Java_SessionDescription_getDescription(jni, j_sdp));
absl::optional<SdpType> sdp_type_maybe = SdpTypeFromString(std_type);
if (!sdp_type_maybe) {
RTC_LOG(LS_ERROR) << "Unexpected SDP type: " << std_type;
return nullptr;
}
// 需要進一步分析這個函式
return CreateSessionDescription(*sdp_type_maybe, std_description);
}
需要進一步分析這個函式 CreateSessionDescription
2.
./pc/jsep_session_description.cc
std::unique_ptr<SessionDescriptionInterface> CreateSessionDescription(
SdpType type,
const std::string& sdp) {
return CreateSessionDescription(type, sdp, nullptr);
}
std::unique_ptr<SessionDescriptionInterface> CreateSessionDescription(
SdpType type,
const std::string& sdp,
SdpParseError* error_out) {
auto jsep_desc = std::make_unique<JsepSessionDescription>(type);
if (type != SdpType::kRollback) {
// 需要進一步分析這個函式
if (!SdpDeserialize(sdp, jsep_desc.get(), error_out)) {
return nullptr;
}
}
return std::move(jsep_desc);
}
需要進一步分析這個函式 SdpDeserialize
3.
./pc/webrtc_sdp.cc
bool SdpDeserialize(const std::string& message,
JsepSessionDescription* jdesc,
SdpParseError* error) {
std::string session_id;
std::string session_version;
TransportDescription session_td("", "");
RtpHeaderExtensions session_extmaps;
rtc::SocketAddress session_connection_addr;
auto desc = std::make_unique<cricket::SessionDescription>();
size_t current_pos = 0;
// Session Description
if (!ParseSessionDescription(message, ¤t_pos, &session_id,
&session_version, &session_td, &session_extmaps,
&session_connection_addr, desc.get(), error)) {
return false;
}
// Media Description
std::vector<std::unique_ptr<JsepIceCandidate>> candidates;
if (!ParseMediaDescription(message, session_td, session_extmaps, ¤t_pos,
session_connection_addr, desc.get(), &candidates,
error)) {
return false;
}
jdesc->Initialize(std::move(desc), session_id, session_version);
for (const auto& candidate : candidates) {
jdesc->AddCandidate(candidate.get());
}
return true;
}
看上述程式碼,我們知道 SDP 解析分為這幾個部分,會話層解析 ParseSessionDescription, 媒體層解析 ParseMediaDescription, 還有 Candidate 的解析(如果存在)
會話層解析
bool ParseSessionDescription(const std::string& message,
size_t* pos,
std::string* session_id,
std::string* session_version,
TransportDescription* session_td,
RtpHeaderExtensions* session_extmaps,
rtc::SocketAddress* connection_addr,
cricket::SessionDescription* desc,
SdpParseError* error) {
std::string line;
desc->set_msid_supported(false);
desc->set_extmap_allow_mixed(false);
// RFC 4566
// v= (protocol version)
if (!GetLineWithType(message, pos, &line, kLineTypeVersion)) {
return ParseFailedExpectLine(message, *pos, kLineTypeVersion, std::string(),
error);
}
// RFC 4566
// o=<username> <sess-id> <sess-version> <nettype> <addrtype>
// <unicast-address>
if (!GetLineWithType(message, pos, &line, kLineTypeOrigin)) {
return ParseFailedExpectLine(message, *pos, kLineTypeOrigin, std::string(),
error);
}
std::vector<std::string> fields;
rtc::split(line.substr(kLinePrefixLength), kSdpDelimiterSpaceChar, &fields);
const size_t expected_fields = 6;
if (fields.size() != expected_fields) {
return ParseFailedExpectFieldNum(line, expected_fields, error);
}
*session_id = fields[1];
*session_version = fields[2];
// RFC 4566
// s= (session name)
if (!GetLineWithType(message, pos, &line, kLineTypeSessionName)) {
return ParseFailedExpectLine(message, *pos, kLineTypeSessionName,
std::string(), error);
}
// absl::optional lines
// Those are the optional lines, so shouldn't return false if not present.
// RFC 4566
// i=* (session information)
GetLineWithType(message, pos, &line, kLineTypeSessionInfo);
// RFC 4566
// u=* (URI of description)
GetLineWithType(message, pos, &line, kLineTypeSessionUri);
// RFC 4566
// e=* (email address)
GetLineWithType(message, pos, &line, kLineTypeSessionEmail);
// RFC 4566
// p=* (phone number)
GetLineWithType(message, pos, &line, kLineTypeSessionPhone);
// RFC 4566
// c=* (connection information -- not required if included in
// all media)
if (GetLineWithType(message, pos, &line, kLineTypeConnection)) {
if (!ParseConnectionData(line, connection_addr, error)) {
return false;
}
}
// RFC 4566
// b=* (zero or more bandwidth information lines)
while (GetLineWithType(message, pos, &line, kLineTypeSessionBandwidth)) {
// By pass zero or more b lines.
}
// RFC 4566
// One or more time descriptions ("t=" and "r=" lines; see below)
// t= (time the session is active)
// r=* (zero or more repeat times)
// Ensure there's at least one time description
if (!GetLineWithType(message, pos, &line, kLineTypeTiming)) {
return ParseFailedExpectLine(message, *pos, kLineTypeTiming, std::string(),
error);
}
while (GetLineWithType(message, pos, &line, kLineTypeRepeatTimes)) {
// By pass zero or more r lines.
}
// Go through the rest of the time descriptions
while (GetLineWithType(message, pos, &line, kLineTypeTiming)) {
while (GetLineWithType(message, pos, &line, kLineTypeRepeatTimes)) {
// By pass zero or more r lines.
}
}
// RFC 4566
// z=* (time zone adjustments)
GetLineWithType(message, pos, &line, kLineTypeTimeZone);
// RFC 4566
// k=* (encryption key)
GetLineWithType(message, pos, &line, kLineTypeEncryptionKey);
// RFC 4566
// a=* (zero or more session attribute lines)
while (GetLineWithType(message, pos, &line, kLineTypeAttributes)) {
if (HasAttribute(line, kAttributeGroup)) {
if (!ParseGroupAttribute(line, desc, error)) {
return false;
}
} else if (HasAttribute(line, kAttributeIceUfrag)) {
if (!GetValue(line, kAttributeIceUfrag, &(session_td->ice_ufrag),
error)) {
return false;
}
} else if (HasAttribute(line, kAttributeIcePwd)) {
if (!GetValue(line, kAttributeIcePwd, &(session_td->ice_pwd), error)) {
return false;
}
} else if (HasAttribute(line, kAttributeIceLite)) {
session_td->ice_mode = cricket::ICEMODE_LITE;
} else if (HasAttribute(line, kAttributeIceOption)) {
if (!ParseIceOptions(line, &(session_td->transport_options), error)) {
return false;
}
} else if (HasAttribute(line, kAttributeFingerprint)) {
if (session_td->identity_fingerprint.get()) {
return ParseFailed(
line,
"Can't have multiple fingerprint attributes at the same level.",
error);
}
std::unique_ptr<rtc::SSLFingerprint> fingerprint;
if (!ParseFingerprintAttribute(line, &fingerprint, error)) {
return false;
}
session_td->identity_fingerprint = std::move(fingerprint);
} else if (HasAttribute(line, kAttributeSetup)) {
if (!ParseDtlsSetup(line, &(session_td->connection_role), error)) {
return false;
}
} else if (HasAttribute(line, kAttributeMsidSemantics)) {
std::string semantics;
if (!GetValue(line, kAttributeMsidSemantics, &semantics, error)) {
return false;
}
desc->set_msid_supported(
CaseInsensitiveFind(semantics, kMediaStreamSemantic));
} else if (HasAttribute(line, kAttributeExtmapAllowMixed)) {
desc->set_extmap_allow_mixed(true);
} else if (HasAttribute(line, kAttributeExtmap)) {
RtpExtension extmap;
if (!ParseExtmap(line, &extmap, error)) {
return false;
}
session_extmaps->push_back(extmap);
}
}
return true;
}
媒體層解析
bool ParseMediaDescription(
const std::string& message,
const TransportDescription& session_td,
const RtpHeaderExtensions& session_extmaps,
size_t* pos,
const rtc::SocketAddress& session_connection_addr,
cricket::SessionDescription* desc,
std::vector<std::unique_ptr<JsepIceCandidate>>* candidates,
SdpParseError* error) {
RTC_DCHECK(desc != NULL);
std::string line;
int mline_index = -1;
int msid_signaling = 0;
// Zero or more media descriptions
// RFC 4566
// m=<media> <port> <proto> <fmt>
while (GetLineWithType(message, pos, &line, kLineTypeMedia)) {
++mline_index;
std::vector<std::string> fields;
rtc::split(line.substr(kLinePrefixLength), kSdpDelimiterSpaceChar, &fields);
const size_t expected_min_fields = 4;
if (fields.size() < expected_min_fields) {
return ParseFailedExpectMinFieldNum(line, expected_min_fields, error);
}
bool port_rejected = false;
// RFC 3264
// To reject an offered stream, the port number in the corresponding stream
// in the answer MUST be set to zero.
if (fields[1] == kMediaPortRejected) {
port_rejected = true;
}
int port = 0;
if (!rtc::FromString<int>(fields[1], &port) || !IsValidPort(port)) {
return ParseFailed(line, "The port number is invalid", error);
}
std::string protocol = fields[2];
// <fmt>
std::vector<int> payload_types;
if (cricket::IsRtpProtocol(protocol)) {
for (size_t j = 3; j < fields.size(); ++j) {
// TODO(wu): Remove when below bug is fixed.
// https://bugzilla.mozilla.org/show_bug.cgi?id=996329
if (fields[j].empty() && j == fields.size() - 1) {
continue;
}
int pl = 0;
if (!GetPayloadTypeFromString(line, fields[j], &pl, error)) {
return false;
}
payload_types.push_back(pl);
}
}
// Make a temporary TransportDescription based on |session_td|.
// Some of this gets overwritten by ParseContent.
TransportDescription transport(
session_td.transport_options, session_td.ice_ufrag, session_td.ice_pwd,
session_td.ice_mode, session_td.connection_role,
session_td.identity_fingerprint.get());
std::unique_ptr<MediaContentDescription> content;
std::string content_name;
bool bundle_only = false;
int section_msid_signaling = 0;
if (HasAttribute(line, kMediaTypeVideo)) {
content = ParseContentDescription<VideoContentDescription>(
message, cricket::MEDIA_TYPE_VIDEO, mline_index, protocol,
payload_types, pos, &content_name, &bundle_only,
§ion_msid_signaling, &transport, candidates, error);
} else if (HasAttribute(line, kMediaTypeAudio)) {
content = ParseContentDescription<AudioContentDescription>(
message, cricket::MEDIA_TYPE_AUDIO, mline_index, protocol,
payload_types, pos, &content_name, &bundle_only,
§ion_msid_signaling, &transport, candidates, error);
} else if (HasAttribute(line, kMediaTypeData)) {
if (cricket::IsDtlsSctp(protocol)) {
// The draft-03 format is:
// m=application <port> DTLS/SCTP <sctp-port>...
// use_sctpmap should be false.
// The draft-26 format is:
// m=application <port> UDP/DTLS/SCTP webrtc-datachannel
// use_sctpmap should be false.
auto data_desc = std::make_unique<SctpDataContentDescription>();
// Default max message size is 64K
// according to draft-ietf-mmusic-sctp-sdp-26
data_desc->set_max_message_size(kDefaultSctpMaxMessageSize);
int p;
if (rtc::FromString(fields[3], &p)) {
data_desc->set_port(p);
} else if (fields[3] == kDefaultSctpmapProtocol) {
data_desc->set_use_sctpmap(false);
}
if (!ParseContent(message, cricket::MEDIA_TYPE_DATA, mline_index,
protocol, payload_types, pos, &content_name,
&bundle_only, §ion_msid_signaling,
data_desc.get(), &transport, candidates, error)) {
return false;
}
data_desc->set_protocol(protocol);
content = std::move(data_desc);
} else {
// RTP
std::unique_ptr<RtpDataContentDescription> data_desc =
ParseContentDescription<RtpDataContentDescription>(
message, cricket::MEDIA_TYPE_DATA, mline_index, protocol,
payload_types, pos, &content_name, &bundle_only,
§ion_msid_signaling, &transport, candidates, error);
content = std::move(data_desc);
}
} else {
RTC_LOG(LS_WARNING) << "Unsupported media type: " << line;
continue;
}
if (!content.get()) {
// ParseContentDescription returns NULL if failed.
return false;
}
msid_signaling |= section_msid_signaling;
bool content_rejected = false;
// A port of 0 is not interpreted as a rejected m= section when it's
// used along with a=bundle-only.
if (bundle_only) {
if (!port_rejected) {
// Usage of bundle-only with a nonzero port is unspecified. So just
// ignore bundle-only if we see this.
bundle_only = false;
RTC_LOG(LS_WARNING)
<< "a=bundle-only attribute observed with a nonzero "
"port; this usage is unspecified so the attribute is being "
"ignored.";
}
} else {
// If not using bundle-only, interpret port 0 in the normal way; the m=
// section is being rejected.
content_rejected = port_rejected;
}
if (cricket::IsRtpProtocol(protocol) && !content->as_sctp()) {
content->set_protocol(protocol);
// Set the extmap.
if (!session_extmaps.empty() &&
!content->rtp_header_extensions().empty()) {
return ParseFailed("",
"The a=extmap MUST be either all session level or "
"all media level.",
error);
}
for (size_t i = 0; i < session_extmaps.size(); ++i) {
content->AddRtpHeaderExtension(session_extmaps[i]);
}
} else if (content->as_sctp()) {
// Do nothing, it's OK
} else {
RTC_LOG(LS_WARNING) << "Parse failed with unknown protocol " << protocol;
return false;
}
// Use the session level connection address if the media level addresses are
// not specified.
rtc::SocketAddress address;
address = content->connection_address().IsNil()
? session_connection_addr
: content->connection_address();
address.SetPort(port);
content->set_connection_address(address);
desc->AddContent(content_name,
cricket::IsDtlsSctp(protocol) ? MediaProtocolType::kSctp
: MediaProtocolType::kRtp,
content_rejected, bundle_only, std::move(content));
// Create TransportInfo with the media level "ice-pwd" and "ice-ufrag".
desc->AddTransportInfo(TransportInfo(content_name, transport));
}
desc->set_msid_signaling(msid_signaling);
size_t end_of_message = message.size();
if (mline_index == -1 && *pos != end_of_message) {
ParseFailed(message, *pos, "Expects m line.", error);
return false;
}
return true;
}
經過上述流程我們知道底層的 SDP 物件其實就是一個 JsepSessionDescription 物件,而 JsepSessionDescription 又包含這個物件 SessionDescription 作為屬性,其實核心資料都在 SessionDescription
底層物件 PeerConnection 的屬性
./pc/peer_connection.h
std::unique_ptr<SessionDescriptionInterface> current_local_description_
std::unique_ptr<SessionDescriptionInterface> pending_local_description_
std::unique_ptr<SessionDescriptionInterface> current_remote_description_
std::unique_ptr<SessionDescriptionInterface> pending_remote_description_
其實也就是 JsepSessionDescription 物件,有關 SDP 的具體資料就是 SessionDescription
good luck!
- WebRTC 中有關 Media Stream & Track & Channel 之間的關係
- smart_rtmpd 推流和拉流那些事
- 為什麼 RTP 的視訊的取樣率是 90kHz ?
- CDN的加速原理是什麼?
- smart_rtmpd 的 NAT 對映方式使用說明
- Mediasoup 雜談(待完善)
- C 獲取當前日期精確到毫秒的幾種方法
- webrtc 中怎麼根據 SDP 建立或關聯底層的 socket 物件?
- webrtc 資料接收流程圖解
- webrtc 有關 SDP 部分的解析流程分析
- smart rtmpd 推流 url 和拉流 url
- Windows 版本的 Webrtc 的編譯 ( 基於聲網映象 )