Add support for sending RTP two-byte header extensions.
Automatic detection if one-byte header or two-byte header should be used based on extension ID and extension length. Bug: webrtc:7990 Change-Id: I9fc848ecc59458d1ca97bace0e57ea04d3d0ced6 Reviewed-on: https://webrtc-review.googlesource.com/c/103422 Commit-Queue: Johannes Kron <kron@webrtc.org> Reviewed-by: Erik Språng <sprang@webrtc.org> Reviewed-by: Seth Hampson <shampson@webrtc.org> Reviewed-by: Danil Chapovalov <danilchap@webrtc.org> Cr-Commit-Position: refs/heads/master@{#25004}
This commit is contained in:

committed by
Commit Bot

parent
7880be1799
commit
78cdde3df6
@ -142,7 +142,9 @@ const char RtpExtension::kEncryptHeaderExtensionsUri[] =
|
||||
|
||||
constexpr int RtpExtension::kMinId;
|
||||
constexpr int RtpExtension::kMaxId;
|
||||
constexpr int RtpExtension::kMaxValueSize;
|
||||
constexpr int RtpExtension::kOneByteHeaderExtensionMaxId;
|
||||
constexpr int RtpExtension::kOneByteHeaderExtensionMaxValueSize;
|
||||
|
||||
bool RtpExtension::IsSupportedForAudio(const std::string& uri) {
|
||||
return uri == webrtc::RtpExtension::kAudioLevelUri ||
|
||||
|
@ -311,7 +311,9 @@ struct RtpExtension {
|
||||
// header extensions, per RFC8285 Section 4.2-4.3.
|
||||
static constexpr int kMinId = 1;
|
||||
static constexpr int kMaxId = 255;
|
||||
static constexpr int kMaxValueSize = 255;
|
||||
static constexpr int kOneByteHeaderExtensionMaxId = 14;
|
||||
static constexpr int kOneByteHeaderExtensionMaxValueSize = 16;
|
||||
|
||||
std::string uri;
|
||||
int id = 0;
|
||||
|
@ -53,10 +53,18 @@ class RtpHeaderExtensionMap {
|
||||
}
|
||||
int32_t Deregister(RTPExtensionType type);
|
||||
|
||||
bool IsMixedOneTwoByteHeaderSupported() const {
|
||||
return mixed_one_two_byte_header_supported_;
|
||||
}
|
||||
void SetMixedOneTwoByteHeaderSupported(bool supported) {
|
||||
mixed_one_two_byte_header_supported_ = supported;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Register(int id, RTPExtensionType type, const char* uri);
|
||||
|
||||
uint8_t ids_[kRtpExtensionNumberOfExtensions];
|
||||
bool mixed_one_two_byte_header_supported_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
@ -56,7 +56,8 @@ static_assert(arraysize(kExtensions) ==
|
||||
constexpr RTPExtensionType RtpHeaderExtensionMap::kInvalidType;
|
||||
constexpr int RtpHeaderExtensionMap::kInvalidId;
|
||||
|
||||
RtpHeaderExtensionMap::RtpHeaderExtensionMap() {
|
||||
RtpHeaderExtensionMap::RtpHeaderExtensionMap()
|
||||
: mixed_one_two_byte_header_supported_(false) {
|
||||
for (auto& id : ids_)
|
||||
id = kInvalidId;
|
||||
}
|
||||
|
@ -18,8 +18,6 @@ int RtpHeaderExtensionSize(rtc::ArrayView<const RtpExtensionSize> extensions,
|
||||
const RtpHeaderExtensionMap& registered_extensions) {
|
||||
// RFC3550 Section 5.3.1
|
||||
static constexpr int kExtensionBlockHeaderSize = 4;
|
||||
// RFC8285 Section 4.2-4.3.
|
||||
static constexpr int kOneByteHeaderExtensionMaxValueSize = 16;
|
||||
|
||||
int values_size = 0;
|
||||
int num_extensions = 0;
|
||||
@ -31,7 +29,8 @@ int RtpHeaderExtensionSize(rtc::ArrayView<const RtpExtensionSize> extensions,
|
||||
// All extensions should use same size header. Check if the |extension|
|
||||
// forces to switch to two byte header that allows larger id and value size.
|
||||
if (id > RtpExtension::kOneByteHeaderExtensionMaxId ||
|
||||
extension.value_size > kOneByteHeaderExtensionMaxValueSize) {
|
||||
extension.value_size >
|
||||
RtpExtension::kOneByteHeaderExtensionMaxValueSize) {
|
||||
each_extension_header_size = 2;
|
||||
}
|
||||
values_size += extension.value_size;
|
||||
|
@ -462,9 +462,11 @@ bool BaseRtpStringExtension::Parse(rtc::ArrayView<const uint8_t> data,
|
||||
|
||||
bool BaseRtpStringExtension::Write(rtc::ArrayView<uint8_t> data,
|
||||
const std::string& str) {
|
||||
if (str.size() > StringRtpHeaderExtension::kMaxSize) {
|
||||
return false;
|
||||
}
|
||||
RTC_DCHECK_EQ(data.size(), str.size());
|
||||
RTC_DCHECK_GE(str.size(), 1);
|
||||
RTC_DCHECK_LE(str.size(), StringRtpHeaderExtension::kMaxSize);
|
||||
memcpy(data.data(), str.data(), str.size());
|
||||
return true;
|
||||
}
|
||||
|
@ -178,10 +178,9 @@ void RtpPacket::SetCsrcs(rtc::ArrayView<const uint32_t> csrcs) {
|
||||
|
||||
rtc::ArrayView<uint8_t> RtpPacket::AllocateRawExtension(int id, size_t length) {
|
||||
RTC_DCHECK_GE(id, RtpExtension::kMinId);
|
||||
RTC_DCHECK_LE(id, RtpExtension::kOneByteHeaderExtensionMaxId);
|
||||
RTC_DCHECK_LE(id, RtpExtension::kMaxId);
|
||||
RTC_DCHECK_GE(length, 1);
|
||||
RTC_DCHECK_LE(length, 16);
|
||||
|
||||
RTC_DCHECK_LE(length, RtpExtension::kMaxValueSize);
|
||||
const ExtensionInfo* extension_entry = FindExtensionInfo(id);
|
||||
if (extension_entry != nullptr) {
|
||||
// Extension already reserved. Check if same length is used.
|
||||
@ -205,10 +204,51 @@ rtc::ArrayView<uint8_t> RtpPacket::AllocateRawExtension(int id, size_t length) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t num_csrc = data()[0] & 0x0F;
|
||||
size_t extensions_offset = kFixedHeaderSize + (num_csrc * 4) + 4;
|
||||
const size_t num_csrc = data()[0] & 0x0F;
|
||||
const size_t extensions_offset = kFixedHeaderSize + (num_csrc * 4) + 4;
|
||||
// Determine if two-byte header is required for the extension based on id and
|
||||
// length. Please note that a length of 0 also requires two-byte header
|
||||
// extension. See RFC8285 Section 4.2-4.3.
|
||||
const bool two_byte_header_required =
|
||||
id > RtpExtension::kOneByteHeaderExtensionMaxId ||
|
||||
length > RtpExtension::kOneByteHeaderExtensionMaxValueSize || length == 0;
|
||||
RTC_CHECK(!two_byte_header_required ||
|
||||
extensions_.IsMixedOneTwoByteHeaderSupported());
|
||||
|
||||
uint16_t profile_id;
|
||||
if (extensions_size_ > 0) {
|
||||
profile_id =
|
||||
ByteReader<uint16_t>::ReadBigEndian(data() + extensions_offset - 4);
|
||||
if (profile_id == kOneByteExtensionProfileId && two_byte_header_required) {
|
||||
// Is buffer size big enough to fit promotion and new data field?
|
||||
// The header extension will grow with one byte per already allocated
|
||||
// extension + the size of the extension that is about to be allocated.
|
||||
size_t expected_new_extensions_size =
|
||||
extensions_size_ + extension_entries_.size() +
|
||||
kTwoByteExtensionHeaderLength + length;
|
||||
if (extensions_offset + expected_new_extensions_size > capacity()) {
|
||||
RTC_LOG(LS_ERROR)
|
||||
<< "Extension cannot be registered: Not enough space left in "
|
||||
"buffer to change to two-byte header extension and add new "
|
||||
"extension.";
|
||||
return nullptr;
|
||||
}
|
||||
// Promote already written data to two-byte header format.
|
||||
PromoteToTwoByteHeaderExtension();
|
||||
profile_id = kTwoByteExtensionProfileId;
|
||||
}
|
||||
} else {
|
||||
// Profile specific ID, set to OneByteExtensionHeader unless
|
||||
// TwoByteExtensionHeader is required.
|
||||
profile_id = two_byte_header_required ? kTwoByteExtensionProfileId
|
||||
: kOneByteExtensionProfileId;
|
||||
}
|
||||
|
||||
const size_t extension_header_size = profile_id == kOneByteExtensionProfileId
|
||||
? kOneByteExtensionHeaderLength
|
||||
: kTwoByteExtensionHeaderLength;
|
||||
size_t new_extensions_size =
|
||||
extensions_size_ + kOneByteExtensionHeaderLength + length;
|
||||
extensions_size_ + extension_header_size + length;
|
||||
if (extensions_offset + new_extensions_size > capacity()) {
|
||||
RTC_LOG(LS_ERROR)
|
||||
<< "Extension cannot be registered: Not enough space left in buffer.";
|
||||
@ -219,22 +259,76 @@ rtc::ArrayView<uint8_t> RtpPacket::AllocateRawExtension(int id, size_t length) {
|
||||
if (extensions_size_ == 0) {
|
||||
RTC_DCHECK_EQ(payload_offset_, kFixedHeaderSize + (num_csrc * 4));
|
||||
WriteAt(0, data()[0] | 0x10); // Set extension bit.
|
||||
// Profile specific ID always set to OneByteExtensionHeader.
|
||||
ByteWriter<uint16_t>::WriteBigEndian(WriteAt(extensions_offset - 4),
|
||||
kOneByteExtensionProfileId);
|
||||
profile_id);
|
||||
}
|
||||
|
||||
uint8_t one_byte_header = rtc::dchecked_cast<uint8_t>(id) << 4;
|
||||
one_byte_header |= rtc::dchecked_cast<uint8_t>(length - 1);
|
||||
WriteAt(extensions_offset + extensions_size_, one_byte_header);
|
||||
if (profile_id == kOneByteExtensionProfileId) {
|
||||
uint8_t one_byte_header = rtc::dchecked_cast<uint8_t>(id) << 4;
|
||||
one_byte_header |= rtc::dchecked_cast<uint8_t>(length - 1);
|
||||
WriteAt(extensions_offset + extensions_size_, one_byte_header);
|
||||
} else {
|
||||
// TwoByteHeaderExtension.
|
||||
uint8_t extension_id = rtc::dchecked_cast<uint8_t>(id);
|
||||
WriteAt(extensions_offset + extensions_size_, extension_id);
|
||||
uint8_t extension_length = rtc::dchecked_cast<uint8_t>(length);
|
||||
WriteAt(extensions_offset + extensions_size_ + 1, extension_length);
|
||||
}
|
||||
|
||||
const uint16_t extension_info_offset = rtc::dchecked_cast<uint16_t>(
|
||||
extensions_offset + extensions_size_ + kOneByteExtensionHeaderLength);
|
||||
extensions_offset + extensions_size_ + extension_header_size);
|
||||
const uint8_t extension_info_length = rtc::dchecked_cast<uint8_t>(length);
|
||||
extension_entries_.emplace_back(id, extension_info_length,
|
||||
extension_info_offset);
|
||||
|
||||
extensions_size_ = new_extensions_size;
|
||||
|
||||
uint16_t extensions_size_padded =
|
||||
SetExtensionLengthMaybeAddZeroPadding(extensions_offset);
|
||||
payload_offset_ = extensions_offset + extensions_size_padded;
|
||||
buffer_.SetSize(payload_offset_);
|
||||
return rtc::MakeArrayView(WriteAt(extension_info_offset),
|
||||
extension_info_length);
|
||||
}
|
||||
|
||||
void RtpPacket::PromoteToTwoByteHeaderExtension() {
|
||||
size_t num_csrc = data()[0] & 0x0F;
|
||||
size_t extensions_offset = kFixedHeaderSize + (num_csrc * 4) + 4;
|
||||
|
||||
RTC_CHECK_GT(extension_entries_.size(), 0);
|
||||
RTC_CHECK_EQ(payload_size_, 0);
|
||||
RTC_CHECK_EQ(kOneByteExtensionProfileId, ByteReader<uint16_t>::ReadBigEndian(
|
||||
data() + extensions_offset - 4));
|
||||
// Rewrite data.
|
||||
// Each extension adds one to the offset. The write-read delta for the last
|
||||
// extension is therefore the same as the number of extension entries.
|
||||
size_t write_read_delta = extension_entries_.size();
|
||||
for (auto extension_entry = extension_entries_.rbegin();
|
||||
extension_entry != extension_entries_.rend(); ++extension_entry) {
|
||||
size_t read_index = extension_entry->offset;
|
||||
size_t write_index = read_index + write_read_delta;
|
||||
// Update offset.
|
||||
extension_entry->offset = rtc::dchecked_cast<uint16_t>(write_index);
|
||||
// Copy data. Use memmove since read/write regions may overlap.
|
||||
memmove(WriteAt(write_index), data() + read_index, extension_entry->length);
|
||||
// Rewrite id and length.
|
||||
WriteAt(--write_index, extension_entry->length);
|
||||
WriteAt(--write_index, extension_entry->id);
|
||||
--write_read_delta;
|
||||
}
|
||||
|
||||
// Update profile header, extensions length, and zero padding.
|
||||
ByteWriter<uint16_t>::WriteBigEndian(WriteAt(extensions_offset - 4),
|
||||
kTwoByteExtensionProfileId);
|
||||
extensions_size_ += extension_entries_.size();
|
||||
uint16_t extensions_size_padded =
|
||||
SetExtensionLengthMaybeAddZeroPadding(extensions_offset);
|
||||
payload_offset_ = extensions_offset + extensions_size_padded;
|
||||
buffer_.SetSize(payload_offset_);
|
||||
}
|
||||
|
||||
uint16_t RtpPacket::SetExtensionLengthMaybeAddZeroPadding(
|
||||
size_t extensions_offset) {
|
||||
// Update header length field.
|
||||
uint16_t extensions_words = rtc::dchecked_cast<uint16_t>(
|
||||
(extensions_size_ + 3) / 4); // Wrap up to 32bit.
|
||||
@ -244,10 +338,7 @@ rtc::ArrayView<uint8_t> RtpPacket::AllocateRawExtension(int id, size_t length) {
|
||||
size_t extension_padding_size = 4 * extensions_words - extensions_size_;
|
||||
memset(WriteAt(extensions_offset + extensions_size_), 0,
|
||||
extension_padding_size);
|
||||
payload_offset_ = extensions_offset + 4 * extensions_words;
|
||||
buffer_.SetSize(payload_offset_);
|
||||
return rtc::MakeArrayView(WriteAt(extension_info_offset),
|
||||
extension_info_length);
|
||||
return 4 * extensions_words;
|
||||
}
|
||||
|
||||
uint8_t* RtpPacket::AllocatePayload(size_t size_bytes) {
|
||||
@ -463,11 +554,22 @@ rtc::ArrayView<const uint8_t> RtpPacket::FindExtension(
|
||||
|
||||
rtc::ArrayView<uint8_t> RtpPacket::AllocateExtension(ExtensionType type,
|
||||
size_t length) {
|
||||
// TODO(webrtc:7990): Add support for empty extensions (length==0).
|
||||
if (length == 0 || length > RtpExtension::kMaxValueSize ||
|
||||
(!extensions_.IsMixedOneTwoByteHeaderSupported() &&
|
||||
length > RtpExtension::kOneByteHeaderExtensionMaxValueSize)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint8_t id = extensions_.GetId(type);
|
||||
if (id == ExtensionManager::kInvalidId) {
|
||||
// Extension not registered.
|
||||
return nullptr;
|
||||
}
|
||||
if (!extensions_.IsMixedOneTwoByteHeaderSupported() &&
|
||||
id > RtpExtension::kOneByteHeaderExtensionMaxId) {
|
||||
return nullptr;
|
||||
}
|
||||
return AllocateRawExtension(id, length);
|
||||
}
|
||||
|
||||
|
@ -143,6 +143,12 @@ class RtpPacket {
|
||||
// Returns empty arrayview on failure.
|
||||
rtc::ArrayView<uint8_t> AllocateRawExtension(int id, size_t length);
|
||||
|
||||
// Promotes existing one-byte header extensions to two-byte header extensions
|
||||
// by rewriting the data and updates the corresponding extension offsets.
|
||||
void PromoteToTwoByteHeaderExtension();
|
||||
|
||||
uint16_t SetExtensionLengthMaybeAddZeroPadding(size_t extensions_offset);
|
||||
|
||||
// Find or allocate an extension |type|. Returns view of size |length|
|
||||
// to write raw extension to or an empty view on failure.
|
||||
rtc::ArrayView<uint8_t> AllocateExtension(ExtensionType type, size_t length);
|
||||
@ -187,8 +193,6 @@ rtc::ArrayView<const uint8_t> RtpPacket::GetRawExtension() const {
|
||||
template <typename Extension, typename... Values>
|
||||
bool RtpPacket::SetExtension(Values... values) {
|
||||
const size_t value_size = Extension::ValueSize(values...);
|
||||
if (value_size == 0 || value_size > 16)
|
||||
return false;
|
||||
auto buffer = AllocateExtension(Extension::kId, value_size);
|
||||
if (buffer.empty())
|
||||
return false;
|
||||
|
@ -64,6 +64,27 @@ constexpr uint8_t kPacketWithTOAndAL[] = {
|
||||
0x12, 0x00, 0x56, 0xce,
|
||||
0x90, 0x80|kAudioLevel, 0x00, 0x00};
|
||||
|
||||
constexpr uint8_t kPacketWithTwoByteExtensionIdLast[] = {
|
||||
0x90, kPayloadType, kSeqNumFirstByte, kSeqNumSecondByte,
|
||||
0x65, 0x43, 0x12, 0x78,
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x10, 0x00, 0x00, 0x04,
|
||||
0x01, 0x03, 0x00, 0x56,
|
||||
0xce, 0x09, 0x01, 0x80|kAudioLevel,
|
||||
kTwoByteExtensionId, 0x03, 0x00, 0x30, // => 0x00 0x30 0x22
|
||||
0x22, 0x00, 0x00, 0x00}; // => Playout delay.min_ms = 3*10
|
||||
// => Playout delay.max_ms = 34*10
|
||||
|
||||
constexpr uint8_t kPacketWithTwoByteExtensionIdFirst[] = {
|
||||
0x90, kPayloadType, kSeqNumFirstByte, kSeqNumSecondByte,
|
||||
0x65, 0x43, 0x12, 0x78,
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x10, 0x00, 0x00, 0x04,
|
||||
kTwoByteExtensionId, 0x03, 0x00, 0x30, // => 0x00 0x30 0x22
|
||||
0x22, 0x01, 0x03, 0x00, // => Playout delay.min_ms = 3*10
|
||||
0x56, 0xce, 0x09, 0x01, // => Playout delay.max_ms = 34*10
|
||||
0x80|kAudioLevel, 0x00, 0x00, 0x00};
|
||||
|
||||
constexpr uint8_t kPacketWithTOAndALInvalidPadding[] = {
|
||||
0x90, kPayloadType, kSeqNumFirstByte, kSeqNumSecondByte,
|
||||
0x65, 0x43, 0x12, 0x78,
|
||||
@ -203,6 +224,51 @@ TEST(RtpPacketTest, CreateWith2Extensions) {
|
||||
ElementsAreArray(packet.data(), packet.size()));
|
||||
}
|
||||
|
||||
TEST(RtpPacketTest, CreateWithTwoByteHeaderExtensionFirst) {
|
||||
RtpPacketToSend::ExtensionManager extensions;
|
||||
extensions.SetMixedOneTwoByteHeaderSupported(true);
|
||||
extensions.Register(kRtpExtensionTransmissionTimeOffset,
|
||||
kTransmissionOffsetExtensionId);
|
||||
extensions.Register(kRtpExtensionAudioLevel, kAudioLevelExtensionId);
|
||||
extensions.Register(kRtpExtensionPlayoutDelay, kTwoByteExtensionId);
|
||||
RtpPacketToSend packet(&extensions);
|
||||
packet.SetPayloadType(kPayloadType);
|
||||
packet.SetSequenceNumber(kSeqNum);
|
||||
packet.SetTimestamp(kTimestamp);
|
||||
packet.SetSsrc(kSsrc);
|
||||
// Set extension that requires two-byte header.
|
||||
PlayoutDelay playoutDelay = {30, 340};
|
||||
ASSERT_TRUE(packet.SetExtension<PlayoutDelayLimits>(playoutDelay));
|
||||
packet.SetExtension<TransmissionOffset>(kTimeOffset);
|
||||
packet.SetExtension<AudioLevel>(kVoiceActive, kAudioLevel);
|
||||
EXPECT_THAT(kPacketWithTwoByteExtensionIdFirst,
|
||||
ElementsAreArray(packet.data(), packet.size()));
|
||||
}
|
||||
|
||||
TEST(RtpPacketTest, CreateWithTwoByteHeaderExtensionLast) {
|
||||
// This test will trigger RtpPacket::PromoteToTwoByteHeaderExtension().
|
||||
RtpPacketToSend::ExtensionManager extensions;
|
||||
extensions.SetMixedOneTwoByteHeaderSupported(true);
|
||||
extensions.Register(kRtpExtensionTransmissionTimeOffset,
|
||||
kTransmissionOffsetExtensionId);
|
||||
extensions.Register(kRtpExtensionAudioLevel, kAudioLevelExtensionId);
|
||||
extensions.Register(kRtpExtensionPlayoutDelay, kTwoByteExtensionId);
|
||||
RtpPacketToSend packet(&extensions);
|
||||
packet.SetPayloadType(kPayloadType);
|
||||
packet.SetSequenceNumber(kSeqNum);
|
||||
packet.SetTimestamp(kTimestamp);
|
||||
packet.SetSsrc(kSsrc);
|
||||
packet.SetExtension<TransmissionOffset>(kTimeOffset);
|
||||
packet.SetExtension<AudioLevel>(kVoiceActive, kAudioLevel);
|
||||
EXPECT_THAT(kPacketWithTOAndAL,
|
||||
ElementsAreArray(packet.data(), packet.size()));
|
||||
// Set extension that requires two-byte header.
|
||||
PlayoutDelay playoutDelay = {30, 340};
|
||||
ASSERT_TRUE(packet.SetExtension<PlayoutDelayLimits>(playoutDelay));
|
||||
EXPECT_THAT(kPacketWithTwoByteExtensionIdLast,
|
||||
ElementsAreArray(packet.data(), packet.size()));
|
||||
}
|
||||
|
||||
TEST(RtpPacketTest, CreateWithDynamicSizedExtensions) {
|
||||
RtpPacketToSend::ExtensionManager extensions;
|
||||
extensions.Register<RtpStreamId>(kRtpStreamIdExtensionId);
|
||||
@ -247,6 +313,14 @@ TEST(RtpPacketTest, TryToCreateWithLongMid) {
|
||||
EXPECT_FALSE(packet.SetExtension<RtpMid>(kLongMid));
|
||||
}
|
||||
|
||||
TEST(RtpPacketTest, TryToCreateTwoByteHeaderNotSupported) {
|
||||
RtpPacketToSend::ExtensionManager extensions;
|
||||
extensions.Register(kRtpExtensionAudioLevel, kTwoByteExtensionId);
|
||||
RtpPacketToSend packet(&extensions);
|
||||
// Set extension that requires two-byte header.
|
||||
EXPECT_FALSE(packet.SetExtension<AudioLevel>(kVoiceActive, kAudioLevel));
|
||||
}
|
||||
|
||||
TEST(RtpPacketTest, CreateWithMaxSizeHeaderExtension) {
|
||||
const std::string kValue = "123456789abcdef";
|
||||
RtpPacket::ExtensionManager extensions;
|
||||
|
Reference in New Issue
Block a user