This CL implements allowing sendonly codecs in setCodecPreferences(), i.e. this spec PR: https://github.com/w3c/webrtc-pc/pull/3018. It also makes the setCodecPreferences() ignore level IDs in the filtering algorithm (but not in the sCP method call) as per this spec PR: https://github.com/w3c/webrtc-pc/pull/3023. In short, before this CL, setCodecPreferences() threw an exception if a codec was preferred that is not present in receiver codec capabilities. After this CL, setCodecPreferences() allows you to prefer codecs that are *either* in the sender capabilities *or* the receiver capabilities. - This allows you to "offer to send", i.e. prefer sendonly codecs on a sendonly transceiver. - The filtering on direction is handled by RtpTransceiver::filtered_codec_preferences() which is called during SDP offer/answer (sdp_offer_answer.cc). Also as per spec changes, if this filtering results in not having any codecs to offer or answer then this results in not having any codec preferences as opposed to throwing an exception (old behavior). - Two old peer_connection_media_unittest.cc tests are updated to reflect the API failing less. This CL adds both unit tests (rtp_transceiver_unittest.cc) and full stack integration tests (peer_connection_encodings_integrationtest.cc). It also makes us pass the following Web Platform Tests in Chrome: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/external/wpt/webrtc/protocol/h265-level-id.https.html Bug: chromium:381407888 Change-Id: I98a5ad1acccb56db0538e4d47975b8a725102c33 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/374520 Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org> Commit-Queue: Henrik Boström <hbos@webrtc.org> Reviewed-by: Harald Alvestrand <hta@webrtc.org> Reviewed-by: Evan Shrubsole <eshr@webrtc.org> Cr-Commit-Position: refs/heads/main@{#43788}
823 lines
36 KiB
C++
823 lines
36 KiB
C++
/*
|
|
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
// This file contains tests for `RtpTransceiver`.
|
|
|
|
#include "pc/rtp_transceiver.h"
|
|
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
#include "absl/strings/string_view.h"
|
|
#include "api/environment/environment_factory.h"
|
|
#include "api/peer_connection_interface.h"
|
|
#include "api/rtp_parameters.h"
|
|
#include "api/test/rtc_error_matchers.h"
|
|
#include "media/base/fake_media_engine.h"
|
|
#include "pc/rtp_parameters_conversion.h"
|
|
#include "pc/test/enable_fake_media.h"
|
|
#include "pc/test/mock_channel_interface.h"
|
|
#include "pc/test/mock_rtp_receiver_internal.h"
|
|
#include "pc/test/mock_rtp_sender_internal.h"
|
|
#include "rtc_base/thread.h"
|
|
#include "test/gmock.h"
|
|
#include "test/gtest.h"
|
|
|
|
using ::testing::_;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Field;
|
|
using ::testing::NiceMock;
|
|
using ::testing::Optional;
|
|
using ::testing::Property;
|
|
using ::testing::Return;
|
|
using ::testing::ReturnRef;
|
|
using ::testing::SizeIs;
|
|
|
|
namespace webrtc {
|
|
|
|
namespace {
|
|
|
|
class RtpTransceiverTest : public testing::Test {
|
|
public:
|
|
RtpTransceiverTest()
|
|
: dependencies_(MakeDependencies()),
|
|
context_(
|
|
ConnectionContext::Create(CreateEnvironment(), &dependencies_)) {}
|
|
|
|
protected:
|
|
cricket::FakeMediaEngine* media_engine() {
|
|
// We know this cast is safe because we supplied the fake implementation
|
|
// in MakeDependencies().
|
|
return static_cast<cricket::FakeMediaEngine*>(context_->media_engine());
|
|
}
|
|
ConnectionContext* context() { return context_.get(); }
|
|
|
|
private:
|
|
rtc::AutoThread main_thread_;
|
|
|
|
static PeerConnectionFactoryDependencies MakeDependencies() {
|
|
PeerConnectionFactoryDependencies d;
|
|
d.network_thread = rtc::Thread::Current();
|
|
d.worker_thread = rtc::Thread::Current();
|
|
d.signaling_thread = rtc::Thread::Current();
|
|
EnableFakeMedia(d, std::make_unique<cricket::FakeMediaEngine>());
|
|
return d;
|
|
}
|
|
|
|
PeerConnectionFactoryDependencies dependencies_;
|
|
rtc::scoped_refptr<ConnectionContext> context_;
|
|
};
|
|
|
|
// Checks that a channel cannot be set on a stopped `RtpTransceiver`.
|
|
TEST_F(RtpTransceiverTest, CannotSetChannelOnStoppedTransceiver) {
|
|
const std::string content_name("my_mid");
|
|
auto transceiver = rtc::make_ref_counted<RtpTransceiver>(
|
|
cricket::MediaType::MEDIA_TYPE_AUDIO, context());
|
|
auto channel1 = std::make_unique<NiceMock<cricket::MockChannelInterface>>();
|
|
EXPECT_CALL(*channel1, media_type())
|
|
.WillRepeatedly(Return(cricket::MediaType::MEDIA_TYPE_AUDIO));
|
|
EXPECT_CALL(*channel1, mid()).WillRepeatedly(ReturnRef(content_name));
|
|
EXPECT_CALL(*channel1, SetFirstPacketReceivedCallback(_));
|
|
EXPECT_CALL(*channel1, SetRtpTransport(_)).WillRepeatedly(Return(true));
|
|
auto channel1_ptr = channel1.get();
|
|
transceiver->SetChannel(std::move(channel1), [&](const std::string& mid) {
|
|
EXPECT_EQ(mid, content_name);
|
|
return nullptr;
|
|
});
|
|
EXPECT_EQ(channel1_ptr, transceiver->channel());
|
|
|
|
// Stop the transceiver.
|
|
transceiver->StopInternal();
|
|
EXPECT_EQ(channel1_ptr, transceiver->channel());
|
|
|
|
auto channel2 = std::make_unique<NiceMock<cricket::MockChannelInterface>>();
|
|
EXPECT_CALL(*channel2, media_type())
|
|
.WillRepeatedly(Return(cricket::MediaType::MEDIA_TYPE_AUDIO));
|
|
|
|
// Clear the current channel - required to allow SetChannel()
|
|
EXPECT_CALL(*channel1_ptr, SetFirstPacketReceivedCallback(_));
|
|
transceiver->ClearChannel();
|
|
ASSERT_EQ(nullptr, transceiver->channel());
|
|
// Channel can no longer be set, so this call should be a no-op.
|
|
transceiver->SetChannel(std::move(channel2),
|
|
[](const std::string&) { return nullptr; });
|
|
EXPECT_EQ(nullptr, transceiver->channel());
|
|
}
|
|
|
|
// Checks that a channel can be unset on a stopped `RtpTransceiver`
|
|
TEST_F(RtpTransceiverTest, CanUnsetChannelOnStoppedTransceiver) {
|
|
const std::string content_name("my_mid");
|
|
auto transceiver = rtc::make_ref_counted<RtpTransceiver>(
|
|
cricket::MediaType::MEDIA_TYPE_VIDEO, context());
|
|
auto channel = std::make_unique<NiceMock<cricket::MockChannelInterface>>();
|
|
EXPECT_CALL(*channel, media_type())
|
|
.WillRepeatedly(Return(cricket::MediaType::MEDIA_TYPE_VIDEO));
|
|
EXPECT_CALL(*channel, mid()).WillRepeatedly(ReturnRef(content_name));
|
|
EXPECT_CALL(*channel, SetFirstPacketReceivedCallback(_))
|
|
.WillRepeatedly(testing::Return());
|
|
EXPECT_CALL(*channel, SetRtpTransport(_)).WillRepeatedly(Return(true));
|
|
|
|
auto channel_ptr = channel.get();
|
|
transceiver->SetChannel(std::move(channel), [&](const std::string& mid) {
|
|
EXPECT_EQ(mid, content_name);
|
|
return nullptr;
|
|
});
|
|
EXPECT_EQ(channel_ptr, transceiver->channel());
|
|
|
|
// Stop the transceiver.
|
|
transceiver->StopInternal();
|
|
EXPECT_EQ(channel_ptr, transceiver->channel());
|
|
|
|
// Set the channel to `nullptr`.
|
|
transceiver->ClearChannel();
|
|
EXPECT_EQ(nullptr, transceiver->channel());
|
|
}
|
|
|
|
class RtpTransceiverUnifiedPlanTest : public RtpTransceiverTest {
|
|
public:
|
|
static rtc::scoped_refptr<MockRtpReceiverInternal> MockReceiver(
|
|
cricket::MediaType media_type) {
|
|
auto receiver = rtc::make_ref_counted<NiceMock<MockRtpReceiverInternal>>();
|
|
EXPECT_CALL(*receiver.get(), media_type())
|
|
.WillRepeatedly(Return(media_type));
|
|
return receiver;
|
|
}
|
|
|
|
static rtc::scoped_refptr<MockRtpSenderInternal> MockSender(
|
|
cricket::MediaType media_type) {
|
|
auto sender = rtc::make_ref_counted<NiceMock<MockRtpSenderInternal>>();
|
|
EXPECT_CALL(*sender.get(), media_type()).WillRepeatedly(Return(media_type));
|
|
return sender;
|
|
}
|
|
|
|
rtc::scoped_refptr<RtpTransceiver> CreateTransceiver(
|
|
rtc::scoped_refptr<RtpSenderInternal> sender,
|
|
rtc::scoped_refptr<RtpReceiverInternal> receiver) {
|
|
return rtc::make_ref_counted<RtpTransceiver>(
|
|
RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
|
|
rtc::Thread::Current(), std::move(sender)),
|
|
RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
|
|
rtc::Thread::Current(), rtc::Thread::Current(),
|
|
std::move(receiver)),
|
|
context(), media_engine()->voice().GetRtpHeaderExtensions(),
|
|
/* on_negotiation_needed= */ [] {});
|
|
}
|
|
|
|
protected:
|
|
rtc::AutoThread main_thread_;
|
|
};
|
|
|
|
// Basic tests for Stop()
|
|
TEST_F(RtpTransceiverUnifiedPlanTest, StopSetsDirection) {
|
|
rtc::scoped_refptr<MockRtpReceiverInternal> receiver =
|
|
MockReceiver(cricket::MediaType::MEDIA_TYPE_AUDIO);
|
|
rtc::scoped_refptr<MockRtpSenderInternal> sender =
|
|
MockSender(cricket::MediaType::MEDIA_TYPE_AUDIO);
|
|
rtc::scoped_refptr<RtpTransceiver> transceiver =
|
|
CreateTransceiver(sender, receiver);
|
|
|
|
EXPECT_CALL(*receiver.get(), Stop());
|
|
EXPECT_CALL(*receiver.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender.get(), Stop());
|
|
|
|
EXPECT_EQ(RtpTransceiverDirection::kInactive, transceiver->direction());
|
|
EXPECT_FALSE(transceiver->current_direction());
|
|
transceiver->StopStandard();
|
|
EXPECT_EQ(RtpTransceiverDirection::kStopped, transceiver->direction());
|
|
EXPECT_FALSE(transceiver->current_direction());
|
|
transceiver->StopTransceiverProcedure();
|
|
EXPECT_TRUE(transceiver->current_direction());
|
|
EXPECT_EQ(RtpTransceiverDirection::kStopped, transceiver->direction());
|
|
EXPECT_EQ(RtpTransceiverDirection::kStopped,
|
|
*transceiver->current_direction());
|
|
}
|
|
|
|
class RtpTransceiverFilteredCodecPreferencesTest
|
|
: public RtpTransceiverUnifiedPlanTest {
|
|
public:
|
|
RtpTransceiverFilteredCodecPreferencesTest()
|
|
: transceiver_(CreateTransceiver(
|
|
MockSender(cricket::MediaType::MEDIA_TYPE_VIDEO),
|
|
MockReceiver(cricket::MediaType::MEDIA_TYPE_VIDEO))) {}
|
|
|
|
struct H264CodecCapabilities {
|
|
cricket::Codec cricket_sendrecv_codec;
|
|
RtpCodecCapability sendrecv_codec;
|
|
cricket::Codec cricket_sendonly_codec;
|
|
RtpCodecCapability sendonly_codec;
|
|
cricket::Codec cricket_recvonly_codec;
|
|
RtpCodecCapability recvonly_codec;
|
|
cricket::Codec cricket_rtx_codec;
|
|
RtpCodecCapability rtx_codec;
|
|
};
|
|
|
|
// For H264, the profile and level IDs are entangled and not ignored by
|
|
// IsSameRtpCodecIgnoringLevel().
|
|
H264CodecCapabilities ConfigureH264CodecCapabilities() {
|
|
cricket::Codec cricket_sendrecv_codec = cricket::CreateVideoCodec(
|
|
SdpVideoFormat("H264",
|
|
{{"level-asymmetry-allowed", "1"},
|
|
{"packetization-mode", "1"},
|
|
{"profile-level-id", "42f00b"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
cricket::Codec cricket_sendonly_codec = cricket::CreateVideoCodec(
|
|
SdpVideoFormat("H264",
|
|
{{"level-asymmetry-allowed", "1"},
|
|
{"packetization-mode", "1"},
|
|
{"profile-level-id", "640034"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
cricket::Codec cricket_recvonly_codec = cricket::CreateVideoCodec(
|
|
SdpVideoFormat("H264",
|
|
{{"level-asymmetry-allowed", "1"},
|
|
{"packetization-mode", "1"},
|
|
{"profile-level-id", "f4001f"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
cricket::Codec cricket_rtx_codec = cricket::CreateVideoRtxCodec(
|
|
cricket::Codec::kIdNotSet, cricket::Codec::kIdNotSet);
|
|
media_engine()->SetVideoSendCodecs(
|
|
{cricket_sendrecv_codec, cricket_sendonly_codec, cricket_rtx_codec});
|
|
media_engine()->SetVideoRecvCodecs(
|
|
{cricket_sendrecv_codec, cricket_recvonly_codec, cricket_rtx_codec});
|
|
return {
|
|
.cricket_sendrecv_codec = cricket_sendrecv_codec,
|
|
.sendrecv_codec = ToRtpCodecCapability(cricket_sendrecv_codec),
|
|
.cricket_sendonly_codec = cricket_sendonly_codec,
|
|
.sendonly_codec = ToRtpCodecCapability(cricket_sendonly_codec),
|
|
.cricket_recvonly_codec = cricket_recvonly_codec,
|
|
.recvonly_codec = ToRtpCodecCapability(cricket_recvonly_codec),
|
|
.cricket_rtx_codec = cricket_rtx_codec,
|
|
.rtx_codec = ToRtpCodecCapability(cricket_rtx_codec),
|
|
};
|
|
}
|
|
|
|
#ifdef RTC_ENABLE_H265
|
|
struct H265CodecCapabilities {
|
|
// The level-id from sender getCapabilities() or receiver getCapabilities().
|
|
static constexpr const char* kSendOnlyLevel = "180";
|
|
static constexpr const char* kRecvOnlyLevel = "156";
|
|
// A valid H265 level-id, but one not present in either getCapabilities().
|
|
static constexpr const char* kLevelNotInCapabilities = "135";
|
|
|
|
cricket::Codec cricket_sendonly_codec;
|
|
RtpCodecCapability sendonly_codec;
|
|
cricket::Codec cricket_recvonly_codec;
|
|
RtpCodecCapability recvonly_codec;
|
|
};
|
|
|
|
// For H265, the profile and level IDs are separate and are ignored by
|
|
// IsSameRtpCodecIgnoringLevel().
|
|
H265CodecCapabilities ConfigureH265CodecCapabilities() {
|
|
cricket::Codec cricket_sendonly_codec = cricket::CreateVideoCodec(
|
|
SdpVideoFormat("H265",
|
|
{{"profile-id", "1"},
|
|
{"tier-flag", "0"},
|
|
{"level-id", H265CodecCapabilities::kSendOnlyLevel},
|
|
{"tx-mode", "SRST"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
cricket::Codec cricket_recvonly_codec = cricket::CreateVideoCodec(
|
|
SdpVideoFormat("H265",
|
|
{{"profile-id", "1"},
|
|
{"tier-flag", "0"},
|
|
{"level-id", H265CodecCapabilities::kRecvOnlyLevel},
|
|
{"tx-mode", "SRST"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
media_engine()->SetVideoSendCodecs({cricket_sendonly_codec});
|
|
media_engine()->SetVideoRecvCodecs({cricket_recvonly_codec});
|
|
return {
|
|
.cricket_sendonly_codec = cricket_sendonly_codec,
|
|
.sendonly_codec = ToRtpCodecCapability(cricket_sendonly_codec),
|
|
.cricket_recvonly_codec = cricket_recvonly_codec,
|
|
.recvonly_codec = ToRtpCodecCapability(cricket_recvonly_codec),
|
|
};
|
|
}
|
|
#endif // RTC_ENABLE_H265
|
|
|
|
protected:
|
|
rtc::scoped_refptr<RtpTransceiver> transceiver_;
|
|
};
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest, EmptyByDefault) {
|
|
ConfigureH264CodecCapabilities();
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendRecv),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(), SizeIs(0));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(), SizeIs(0));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(), SizeIs(0));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kInactive),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(), SizeIs(0));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest, OrderIsMaintained) {
|
|
const auto codecs = ConfigureH264CodecCapabilities();
|
|
std::vector<RtpCodecCapability> codec_capabilities = {codecs.sendrecv_codec,
|
|
codecs.rtx_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codec_capabilities[0], codec_capabilities[1]));
|
|
// Reverse order.
|
|
codec_capabilities = {codecs.rtx_codec, codecs.sendrecv_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codec_capabilities[0], codec_capabilities[1]));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest,
|
|
FiltersCodecsBasedOnDirection) {
|
|
const auto codecs = ConfigureH264CodecCapabilities();
|
|
std::vector<RtpCodecCapability> codec_capabilities = {
|
|
codecs.sendonly_codec, codecs.sendrecv_codec, codecs.recvonly_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendRecv),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.sendrecv_codec));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.sendonly_codec, codecs.sendrecv_codec));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.sendrecv_codec, codecs.recvonly_codec));
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kInactive),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.sendrecv_codec));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest,
|
|
RtxIsIncludedAfterFiltering) {
|
|
const auto codecs = ConfigureH264CodecCapabilities();
|
|
std::vector<RtpCodecCapability> codec_capabilities = {codecs.recvonly_codec,
|
|
codecs.rtx_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.recvonly_codec, codecs.rtx_codec));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest,
|
|
NoMediaIsTheSameAsNoPreference) {
|
|
const auto codecs = ConfigureH264CodecCapabilities();
|
|
std::vector<RtpCodecCapability> codec_capabilities = {codecs.recvonly_codec,
|
|
codecs.rtx_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendOnly),
|
|
IsRtcOk());
|
|
// After filtering the only codec that remains is RTX which is not a media
|
|
// codec, this is the same as not having any preferences.
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(), SizeIs(0));
|
|
|
|
// But the preferences are remembered in case the direction changes such that
|
|
// we do have a media codec.
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codecs.recvonly_codec, codecs.rtx_codec));
|
|
}
|
|
|
|
#ifdef RTC_ENABLE_H265
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest,
|
|
H265LevelIdIsIgnoredByFilter) {
|
|
const auto codecs = ConfigureH265CodecCapabilities();
|
|
std::vector<RtpCodecCapability> codec_capabilities = {codecs.sendonly_codec,
|
|
codecs.recvonly_codec};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities), IsRtcOk());
|
|
// Regardless of direction, both codecs are preferred due to ignoring levels.
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codec_capabilities[0], codec_capabilities[1]));
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kRecvOnly),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codec_capabilities[0], codec_capabilities[1]));
|
|
EXPECT_THAT(
|
|
transceiver_->SetDirectionWithError(RtpTransceiverDirection::kSendRecv),
|
|
IsRtcOk());
|
|
EXPECT_THAT(transceiver_->filtered_codec_preferences(),
|
|
ElementsAre(codec_capabilities[0], codec_capabilities[1]));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverFilteredCodecPreferencesTest,
|
|
H265LevelIdHasToFromSenderOrReceiverCapabilities) {
|
|
ConfigureH265CodecCapabilities();
|
|
cricket::Codec cricket_codec = cricket::CreateVideoCodec(SdpVideoFormat(
|
|
"H265",
|
|
{{"profile-id", "1"},
|
|
{"tier-flag", "0"},
|
|
{"level-id", H265CodecCapabilities::kLevelNotInCapabilities},
|
|
{"tx-mode", "SRST"}},
|
|
{ScalabilityMode::kL1T1}));
|
|
|
|
std::vector<RtpCodecCapability> codec_capabilities = {
|
|
ToRtpCodecCapability(cricket_codec)};
|
|
EXPECT_THAT(transceiver_->SetCodecPreferences(codec_capabilities),
|
|
IsRtcErrorWithTypeAndMessage(
|
|
RTCErrorType::INVALID_MODIFICATION,
|
|
"Invalid codec preferences: Missing codec from codec "
|
|
"capabilities."));
|
|
}
|
|
#endif // RTC_ENABLE_H265
|
|
|
|
class RtpTransceiverTestForHeaderExtensions
|
|
: public RtpTransceiverUnifiedPlanTest {
|
|
public:
|
|
RtpTransceiverTestForHeaderExtensions()
|
|
: extensions_(
|
|
{RtpHeaderExtensionCapability("uri1",
|
|
1,
|
|
RtpTransceiverDirection::kSendOnly),
|
|
RtpHeaderExtensionCapability("uri2",
|
|
2,
|
|
RtpTransceiverDirection::kRecvOnly),
|
|
RtpHeaderExtensionCapability(RtpExtension::kMidUri,
|
|
3,
|
|
RtpTransceiverDirection::kSendRecv),
|
|
RtpHeaderExtensionCapability(RtpExtension::kVideoRotationUri,
|
|
4,
|
|
RtpTransceiverDirection::kSendRecv)}),
|
|
transceiver_(rtc::make_ref_counted<RtpTransceiver>(
|
|
RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
|
|
rtc::Thread::Current(),
|
|
sender_),
|
|
RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
|
|
rtc::Thread::Current(),
|
|
rtc::Thread::Current(),
|
|
receiver_),
|
|
context(),
|
|
extensions_,
|
|
/* on_negotiation_needed= */ [] {})) {}
|
|
|
|
void ClearChannel() {
|
|
EXPECT_CALL(*sender_.get(), SetMediaChannel(_));
|
|
transceiver_->ClearChannel();
|
|
}
|
|
|
|
rtc::scoped_refptr<MockRtpReceiverInternal> receiver_ =
|
|
MockReceiver(cricket::MediaType::MEDIA_TYPE_AUDIO);
|
|
rtc::scoped_refptr<MockRtpSenderInternal> sender_ =
|
|
MockSender(cricket::MediaType::MEDIA_TYPE_AUDIO);
|
|
|
|
std::vector<RtpHeaderExtensionCapability> extensions_;
|
|
rtc::scoped_refptr<RtpTransceiver> transceiver_;
|
|
};
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, OffersChannelManagerList) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(), extensions_);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, ModifiesDirection) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto modified_extensions = extensions_;
|
|
modified_extensions[0].direction = RtpTransceiverDirection::kSendOnly;
|
|
EXPECT_TRUE(
|
|
transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions).ok());
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(),
|
|
modified_extensions);
|
|
modified_extensions[0].direction = RtpTransceiverDirection::kRecvOnly;
|
|
EXPECT_TRUE(
|
|
transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions).ok());
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(),
|
|
modified_extensions);
|
|
modified_extensions[0].direction = RtpTransceiverDirection::kSendRecv;
|
|
EXPECT_TRUE(
|
|
transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions).ok());
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(),
|
|
modified_extensions);
|
|
modified_extensions[0].direction = RtpTransceiverDirection::kInactive;
|
|
EXPECT_TRUE(
|
|
transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions).ok());
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(),
|
|
modified_extensions);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, AcceptsStoppedExtension) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto modified_extensions = extensions_;
|
|
modified_extensions[0].direction = RtpTransceiverDirection::kStopped;
|
|
EXPECT_TRUE(
|
|
transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions).ok());
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(),
|
|
modified_extensions);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, RejectsDifferentSize) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto modified_extensions = extensions_;
|
|
modified_extensions.pop_back();
|
|
|
|
EXPECT_THAT(transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions),
|
|
Property(&RTCError::type, RTCErrorType::INVALID_MODIFICATION));
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(), extensions_);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, RejectsChangedUri) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto modified_extensions = extensions_;
|
|
ASSERT_TRUE(!modified_extensions.empty());
|
|
modified_extensions[0].uri = "http://webrtc.org";
|
|
|
|
EXPECT_THAT(transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions),
|
|
Property(&RTCError::type, RTCErrorType::INVALID_MODIFICATION));
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(), extensions_);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, RejectsReorder) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto modified_extensions = extensions_;
|
|
ASSERT_GE(modified_extensions.size(), 2u);
|
|
std::swap(modified_extensions[0], modified_extensions[1]);
|
|
|
|
EXPECT_THAT(transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions),
|
|
Property(&RTCError::type, RTCErrorType::INVALID_MODIFICATION));
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(), extensions_);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions,
|
|
RejectsStoppedMandatoryExtensions) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
std::vector<RtpHeaderExtensionCapability> modified_extensions = extensions_;
|
|
// Attempting to stop the mandatory MID extension.
|
|
modified_extensions[2].direction = RtpTransceiverDirection::kStopped;
|
|
EXPECT_THAT(transceiver_->SetHeaderExtensionsToNegotiate(modified_extensions),
|
|
Property(&RTCError::type, RTCErrorType::INVALID_MODIFICATION));
|
|
EXPECT_EQ(transceiver_->GetHeaderExtensionsToNegotiate(), extensions_);
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions,
|
|
NoNegotiatedHdrExtsWithoutChannel) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
EXPECT_THAT(transceiver_->GetNegotiatedHeaderExtensions(),
|
|
ElementsAre(Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped)));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions,
|
|
NoNegotiatedHdrExtsWithChannelWithoutNegotiation) {
|
|
const std::string content_name("my_mid");
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_)).WillRepeatedly(Return());
|
|
EXPECT_CALL(*receiver_.get(), Stop()).WillRepeatedly(Return());
|
|
EXPECT_CALL(*sender_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
auto mock_channel =
|
|
std::make_unique<NiceMock<cricket::MockChannelInterface>>();
|
|
auto mock_channel_ptr = mock_channel.get();
|
|
EXPECT_CALL(*mock_channel, SetFirstPacketReceivedCallback(_));
|
|
EXPECT_CALL(*mock_channel, media_type())
|
|
.WillRepeatedly(Return(cricket::MediaType::MEDIA_TYPE_AUDIO));
|
|
EXPECT_CALL(*mock_channel, voice_media_send_channel())
|
|
.WillRepeatedly(Return(nullptr));
|
|
EXPECT_CALL(*mock_channel, mid()).WillRepeatedly(ReturnRef(content_name));
|
|
EXPECT_CALL(*mock_channel, SetRtpTransport(_)).WillRepeatedly(Return(true));
|
|
transceiver_->SetChannel(std::move(mock_channel),
|
|
[](const std::string&) { return nullptr; });
|
|
EXPECT_THAT(transceiver_->GetNegotiatedHeaderExtensions(),
|
|
ElementsAre(Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped)));
|
|
|
|
EXPECT_CALL(*mock_channel_ptr, SetFirstPacketReceivedCallback(_));
|
|
ClearChannel();
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions, ReturnsNegotiatedHdrExts) {
|
|
const std::string content_name("my_mid");
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_)).WillRepeatedly(Return());
|
|
EXPECT_CALL(*receiver_.get(), Stop()).WillRepeatedly(Return());
|
|
EXPECT_CALL(*sender_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
auto mock_channel =
|
|
std::make_unique<NiceMock<cricket::MockChannelInterface>>();
|
|
auto mock_channel_ptr = mock_channel.get();
|
|
EXPECT_CALL(*mock_channel, SetFirstPacketReceivedCallback(_));
|
|
EXPECT_CALL(*mock_channel, media_type())
|
|
.WillRepeatedly(Return(cricket::MediaType::MEDIA_TYPE_AUDIO));
|
|
EXPECT_CALL(*mock_channel, voice_media_send_channel())
|
|
.WillRepeatedly(Return(nullptr));
|
|
EXPECT_CALL(*mock_channel, mid()).WillRepeatedly(ReturnRef(content_name));
|
|
EXPECT_CALL(*mock_channel, SetRtpTransport(_)).WillRepeatedly(Return(true));
|
|
|
|
cricket::RtpHeaderExtensions extensions = {RtpExtension("uri1", 1),
|
|
RtpExtension("uri2", 2)};
|
|
cricket::AudioContentDescription description;
|
|
description.set_rtp_header_extensions(extensions);
|
|
transceiver_->OnNegotiationUpdate(SdpType::kAnswer, &description);
|
|
|
|
transceiver_->SetChannel(std::move(mock_channel),
|
|
[](const std::string&) { return nullptr; });
|
|
|
|
EXPECT_THAT(transceiver_->GetNegotiatedHeaderExtensions(),
|
|
ElementsAre(Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kSendRecv),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kSendRecv),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped)));
|
|
EXPECT_CALL(*mock_channel_ptr, SetFirstPacketReceivedCallback(_));
|
|
ClearChannel();
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions,
|
|
ReturnsNegotiatedHdrExtsSecondTime) {
|
|
EXPECT_CALL(*receiver_.get(), Stop());
|
|
EXPECT_CALL(*receiver_.get(), SetMediaChannel(_));
|
|
EXPECT_CALL(*sender_.get(), SetTransceiverAsStopped());
|
|
EXPECT_CALL(*sender_.get(), Stop());
|
|
|
|
cricket::RtpHeaderExtensions extensions = {RtpExtension("uri1", 1),
|
|
RtpExtension("uri2", 2)};
|
|
cricket::AudioContentDescription description;
|
|
description.set_rtp_header_extensions(extensions);
|
|
transceiver_->OnNegotiationUpdate(SdpType::kAnswer, &description);
|
|
|
|
EXPECT_THAT(transceiver_->GetNegotiatedHeaderExtensions(),
|
|
ElementsAre(Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kSendRecv),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kSendRecv),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped)));
|
|
extensions = {RtpExtension("uri3", 4), RtpExtension("uri5", 6)};
|
|
description.set_rtp_header_extensions(extensions);
|
|
transceiver_->OnNegotiationUpdate(SdpType::kAnswer, &description);
|
|
|
|
EXPECT_THAT(transceiver_->GetNegotiatedHeaderExtensions(),
|
|
ElementsAre(Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped),
|
|
Field(&RtpHeaderExtensionCapability::direction,
|
|
RtpTransceiverDirection::kStopped)));
|
|
}
|
|
|
|
TEST_F(RtpTransceiverTestForHeaderExtensions,
|
|
SimulcastOrSvcEnablesExtensionsByDefault) {
|
|
std::vector<RtpHeaderExtensionCapability> extensions = {
|
|
{RtpExtension::kDependencyDescriptorUri, 1,
|
|
RtpTransceiverDirection::kStopped},
|
|
{RtpExtension::kVideoLayersAllocationUri, 2,
|
|
RtpTransceiverDirection::kStopped},
|
|
};
|
|
|
|
// Default is stopped.
|
|
auto sender = rtc::make_ref_counted<NiceMock<MockRtpSenderInternal>>();
|
|
auto transceiver = rtc::make_ref_counted<RtpTransceiver>(
|
|
RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
|
|
rtc::Thread::Current(), sender),
|
|
RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
|
|
rtc::Thread::Current(), rtc::Thread::Current(), receiver_),
|
|
context(), extensions,
|
|
/* on_negotiation_needed= */ [] {});
|
|
std::vector<webrtc::RtpHeaderExtensionCapability> header_extensions =
|
|
transceiver->GetHeaderExtensionsToNegotiate();
|
|
ASSERT_EQ(header_extensions.size(), 2u);
|
|
EXPECT_EQ(header_extensions[0].uri, RtpExtension::kDependencyDescriptorUri);
|
|
EXPECT_EQ(header_extensions[0].direction, RtpTransceiverDirection::kStopped);
|
|
EXPECT_EQ(header_extensions[1].uri, RtpExtension::kVideoLayersAllocationUri);
|
|
EXPECT_EQ(header_extensions[1].direction, RtpTransceiverDirection::kStopped);
|
|
|
|
// Simulcast, i.e. more than one encoding.
|
|
RtpParameters simulcast_parameters;
|
|
simulcast_parameters.encodings.resize(2);
|
|
auto simulcast_sender =
|
|
rtc::make_ref_counted<NiceMock<MockRtpSenderInternal>>();
|
|
EXPECT_CALL(*simulcast_sender, GetParametersInternal())
|
|
.WillRepeatedly(Return(simulcast_parameters));
|
|
auto simulcast_transceiver = rtc::make_ref_counted<RtpTransceiver>(
|
|
RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
|
|
rtc::Thread::Current(), simulcast_sender),
|
|
RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
|
|
rtc::Thread::Current(), rtc::Thread::Current(), receiver_),
|
|
context(), extensions,
|
|
/* on_negotiation_needed= */ [] {});
|
|
auto simulcast_extensions =
|
|
simulcast_transceiver->GetHeaderExtensionsToNegotiate();
|
|
ASSERT_EQ(simulcast_extensions.size(), 2u);
|
|
EXPECT_EQ(simulcast_extensions[0].uri,
|
|
RtpExtension::kDependencyDescriptorUri);
|
|
EXPECT_EQ(simulcast_extensions[0].direction,
|
|
RtpTransceiverDirection::kSendRecv);
|
|
EXPECT_EQ(simulcast_extensions[1].uri,
|
|
RtpExtension::kVideoLayersAllocationUri);
|
|
EXPECT_EQ(simulcast_extensions[1].direction,
|
|
RtpTransceiverDirection::kSendRecv);
|
|
|
|
// SVC, a single encoding with a scalabilityMode other than L1T1.
|
|
webrtc::RtpParameters svc_parameters;
|
|
svc_parameters.encodings.resize(1);
|
|
svc_parameters.encodings[0].scalability_mode = "L3T3";
|
|
|
|
auto svc_sender = rtc::make_ref_counted<NiceMock<MockRtpSenderInternal>>();
|
|
EXPECT_CALL(*svc_sender, GetParametersInternal())
|
|
.WillRepeatedly(Return(svc_parameters));
|
|
auto svc_transceiver = rtc::make_ref_counted<RtpTransceiver>(
|
|
RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
|
|
rtc::Thread::Current(), svc_sender),
|
|
RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
|
|
rtc::Thread::Current(), rtc::Thread::Current(), receiver_),
|
|
context(), extensions,
|
|
/* on_negotiation_needed= */ [] {});
|
|
std::vector<webrtc::RtpHeaderExtensionCapability> svc_extensions =
|
|
svc_transceiver->GetHeaderExtensionsToNegotiate();
|
|
ASSERT_EQ(svc_extensions.size(), 2u);
|
|
EXPECT_EQ(svc_extensions[0].uri, RtpExtension::kDependencyDescriptorUri);
|
|
EXPECT_EQ(svc_extensions[0].direction, RtpTransceiverDirection::kSendRecv);
|
|
EXPECT_EQ(svc_extensions[1].uri, RtpExtension::kVideoLayersAllocationUri);
|
|
EXPECT_EQ(svc_extensions[1].direction, RtpTransceiverDirection::kSendRecv);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
} // namespace webrtc
|