blob: 2eab588f40b13b132f6fbb1cce7733206335f066 [file]
/*
* Copyright 2026 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.
*/
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/strings/string_view.h"
#include "api/create_modular_peer_connection_factory.h"
#include "api/data_channel_interface.h"
#include "api/jsep.h"
#include "api/make_ref_counted.h"
#include "api/media_stream_interface.h"
#include "api/media_types.h"
#include "api/peer_connection_interface.h"
#include "api/peer_connection_tracer_interface.h"
#include "api/rtc_error.h"
#include "api/rtp_transceiver_direction.h"
#include "api/rtp_transceiver_interface.h"
#include "api/scoped_refptr.h"
#include "pc/peer_connection.h"
#include "pc/peer_connection_wrapper.h"
#include "pc/test/enable_fake_media.h"
#include "pc/test/mock_peer_connection_observers.h"
#include "rtc_base/thread.h"
#include "rtc_base/virtual_socket_server.h"
#include "test/create_test_environment.h"
#include "test/gtest.h"
#include "test/run_loop.h"
#include "test/wait_until.h"
namespace webrtc {
namespace {
using RTCConfiguration = PeerConnectionInterface::RTCConfiguration;
// Records how many times each tracer method has been invoked. Tests retain a
// raw pointer to the installed tracer (the PC owns it) and assert against the
// counts after driving operations.
class CountingTracer : public PeerConnectionTracerInterface {
public:
struct Counts {
int create_offer = 0;
int create_offer_success = 0;
int create_offer_failure = 0;
int create_answer = 0;
int create_answer_success = 0;
int create_answer_failure = 0;
int set_local_description = 0;
int set_local_description_implicit = 0;
int set_local_description_success = 0;
int set_local_description_success_with_sdp = 0;
int set_local_description_failure = 0;
int set_remote_description = 0;
int set_remote_description_success = 0;
int set_remote_description_failure = 0;
int create = 0;
int set_configuration = 0;
int restart_ice = 0;
int close = 0;
int ice_candidate = 0;
int add_ice_candidate = 0;
int add_ice_candidate_success = 0;
int add_ice_candidate_failure = 0;
int ice_candidate_error = 0;
int data_channel_created_local = 0;
int data_channel_created_remote = 0;
std::optional<int> data_channel_id;
int add_transceiver = 0;
std::optional<RtpTransceiverDirection> add_transceiver_direction;
int add_track = 0;
std::vector<std::string> add_track_stream_ids;
int track = 0;
int signaling_state_changed = 0;
int ice_connection_state_changed = 0;
int connection_state_changed = 0;
int ice_gathering_state_changed = 0;
int negotiation_needed_event = 0;
};
const Counts& counts() const { return counts_; }
void OnCreateOffer(
const PeerConnectionInterface::RTCOfferAnswerOptions&) override {
counts_.create_offer++;
}
void OnCreateOfferSuccess(const SessionDescriptionInterface*) override {
counts_.create_offer_success++;
}
void OnCreateOfferFailure(const RTCError&) override {
counts_.create_offer_failure++;
}
void OnCreateAnswer(
const PeerConnectionInterface::RTCOfferAnswerOptions&) override {
counts_.create_answer++;
}
void OnCreateAnswerSuccess(const SessionDescriptionInterface*) override {
counts_.create_answer_success++;
}
void OnCreateAnswerFailure(const RTCError&) override {
counts_.create_answer_failure++;
}
void OnSetLocalDescription(
const SessionDescriptionInterface* description) override {
counts_.set_local_description++;
if (description == nullptr) {
counts_.set_local_description_implicit++;
}
}
void OnSetLocalDescriptionSuccess(
const SessionDescriptionInterface* description) override {
counts_.set_local_description_success++;
if (description != nullptr) {
counts_.set_local_description_success_with_sdp++;
}
}
void OnSetLocalDescriptionFailure(const RTCError&) override {
counts_.set_local_description_failure++;
}
void OnSetRemoteDescription(const SessionDescriptionInterface*) override {
counts_.set_remote_description++;
}
void OnSetRemoteDescriptionSuccess() override {
counts_.set_remote_description_success++;
}
void OnSetRemoteDescriptionFailure(const RTCError&) override {
counts_.set_remote_description_failure++;
}
void OnCreate(
const PeerConnectionInterface::RTCConfiguration& /*configuration*/)
override {
counts_.create++;
}
void OnSetConfiguration(
const PeerConnectionInterface::RTCConfiguration&) override {
counts_.set_configuration++;
}
void OnRestartIce() override { counts_.restart_ice++; }
void OnClose() override { counts_.close++; }
void OnIceCandidate(const IceCandidate&) override { counts_.ice_candidate++; }
void OnAddIceCandidate(const IceCandidate&) override {
counts_.add_ice_candidate++;
}
void OnAddIceCandidateSuccess() override {
counts_.add_ice_candidate_success++;
}
void OnAddIceCandidateFailure(const RTCError&) override {
counts_.add_ice_candidate_failure++;
}
void OnIceCandidateError(absl::string_view,
int,
absl::string_view,
int,
absl::string_view) override {
counts_.ice_candidate_error++;
}
void OnCreateDataChannel(const DataChannelInterface&,
std::optional<int> id) override {
counts_.data_channel_created_local++;
counts_.data_channel_id = id;
}
void OnDataChannel(const DataChannelInterface&,
std::optional<int> id) override {
counts_.data_channel_created_remote++;
counts_.data_channel_id = id;
}
void OnAddTransceiver(MediaType,
const MediaStreamTrackInterface*,
const RtpTransceiverInit& init) override {
counts_.add_transceiver++;
counts_.add_transceiver_direction = init.direction;
}
void OnAddTrack(const MediaStreamTrackInterface&,
const std::vector<std::string>& stream_ids) override {
counts_.add_track++;
counts_.add_track_stream_ids = stream_ids;
}
void OnTrack(const RtpTransceiverInterface&) override { counts_.track++; }
void OnSignalingStateChanged(
PeerConnectionInterface::SignalingState) override {
counts_.signaling_state_changed++;
}
void OnIceConnectionStateChanged(
PeerConnectionInterface::IceConnectionState) override {
counts_.ice_connection_state_changed++;
}
void OnConnectionStateChanged(
PeerConnectionInterface::PeerConnectionState) override {
counts_.connection_state_changed++;
}
void OnIceGatheringStateChanged(
PeerConnectionInterface::IceGatheringState) override {
counts_.ice_gathering_state_changed++;
}
void OnNegotiationNeeded() override { counts_.negotiation_needed_event++; }
private:
Counts counts_;
};
PeerConnectionFactoryDependencies CreateFactoryDependencies() {
PeerConnectionFactoryDependencies deps;
deps.env = CreateTestEnvironment();
deps.network_thread = Thread::Current();
deps.worker_thread = Thread::Current();
deps.signaling_thread = Thread::Current();
EnableFakeMedia(deps);
return deps;
}
class PeerConnectionTracerTest : public ::testing::Test {
protected:
PeerConnectionTracerTest()
: vss_(std::make_unique<VirtualSocketServer>()), main_(vss_.get()) {
pc_factory_ =
CreateModularPeerConnectionFactory(CreateFactoryDependencies());
}
// Creates a PC with a freshly attached CountingTracer. The tracer is owned
// by the PC; tests recover it via Counts() (or by hand through
// pc->GetInternalPeerConnection()->tracer()).
std::unique_ptr<PeerConnectionWrapper> CreatePeerConnection() {
auto observer = std::make_unique<MockPeerConnectionObserver>();
PeerConnectionDependencies deps(observer.get());
deps.tracer = std::make_unique<CountingTracer>();
RTCConfiguration config;
config.sdp_semantics = SdpSemantics::kUnifiedPlan;
auto result =
pc_factory_->CreatePeerConnectionOrError(config, std::move(deps));
if (!result.ok()) {
return nullptr;
}
observer->SetPeerConnectionInterface(result.value().get());
return std::make_unique<PeerConnectionWrapper>(
pc_factory_, result.MoveValue(), std::move(observer));
}
// Returns the CountingTracer attached to `pc`, recovered through the
// tracer() accessor on PeerConnection.
const CountingTracer::Counts& Counts(const PeerConnectionWrapper& pc) {
return static_cast<const CountingTracer*>(
pc.GetInternalPeerConnection()->tracer())
->counts();
}
std::unique_ptr<VirtualSocketServer> vss_;
test::RunLoop main_;
scoped_refptr<PeerConnectionFactoryInterface> pc_factory_;
};
// At construction time the tracer should see exactly one OnCreate and nothing
// else. In particular the configuration is reported there, not as an
// OnSetConfiguration the application never triggered.
TEST_F(PeerConnectionTracerTest, FiresOnCreate) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
EXPECT_EQ(Counts(*pc).create, 1);
EXPECT_EQ(Counts(*pc).set_configuration, 0);
EXPECT_EQ(Counts(*pc).create_offer, 0);
EXPECT_EQ(Counts(*pc).close, 0);
}
// CreateOffer should fire OnCreateOffer once and OnCreateOfferSuccess once
// when the operation resolves successfully.
TEST_F(PeerConnectionTracerTest, FiresOnCreateOffer) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
pc->AddAudioTrack("a");
auto offer = pc->CreateOffer();
ASSERT_TRUE(offer);
EXPECT_EQ(Counts(*pc).create_offer, 1);
EXPECT_EQ(Counts(*pc).create_offer_success, 1);
EXPECT_EQ(Counts(*pc).create_offer_failure, 0);
}
// SetLocalDescription with an explicit description should fire
// OnSetLocalDescription with that description and report success.
TEST_F(PeerConnectionTracerTest, FiresOnSetLocalDescription) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
pc->AddAudioTrack("a");
auto offer = pc->CreateOffer();
ASSERT_TRUE(offer);
ASSERT_TRUE(pc->SetLocalDescription(std::move(offer)));
EXPECT_EQ(Counts(*pc).set_local_description, 1);
EXPECT_EQ(Counts(*pc).set_local_description_implicit, 0);
EXPECT_EQ(Counts(*pc).set_local_description_success, 1);
EXPECT_EQ(Counts(*pc).set_local_description_success_with_sdp, 1);
EXPECT_EQ(Counts(*pc).set_local_description_failure, 0);
}
// CreateDataChannel should fire OnCreateDataChannel.
TEST_F(PeerConnectionTracerTest, FiresOnCreateDataChannel) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
auto channel = pc->CreateDataChannel("dc");
ASSERT_TRUE(channel);
EXPECT_EQ(Counts(*pc).data_channel_created_local, 1);
EXPECT_EQ(Counts(*pc).data_channel_created_remote, 0);
EXPECT_EQ(Counts(*pc).data_channel_id, std::nullopt);
}
// A negotiated channel carries the id the application picked.
TEST_F(PeerConnectionTracerTest, FiresOnCreateDataChannelWithNegotiatedId) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
DataChannelInit config;
config.negotiated = true;
config.id = 7;
auto channel = pc->pc()->CreateDataChannelOrError("dc", &config);
ASSERT_TRUE(channel.ok());
EXPECT_EQ(Counts(*pc).data_channel_created_local, 1);
EXPECT_EQ(Counts(*pc).data_channel_id, 7);
}
// SetConfiguration should fire OnSetConfiguration once it succeeded.
TEST_F(PeerConnectionTracerTest, FiresOnSetConfiguration) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
ASSERT_EQ(Counts(*pc).set_configuration, 0);
RTCConfiguration config;
config.sdp_semantics = SdpSemantics::kUnifiedPlan;
ASSERT_TRUE(pc->pc()->SetConfiguration(config).ok());
EXPECT_EQ(Counts(*pc).set_configuration, 1);
}
// RestartIce should fire OnRestartIce.
TEST_F(PeerConnectionTracerTest, FiresOnRestartIce) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
ASSERT_EQ(Counts(*pc).restart_ice, 0);
pc->pc()->RestartIce();
EXPECT_EQ(Counts(*pc).restart_ice, 1);
}
// Close should fire OnClose once and at least one signaling-state transition
// (to "closed").
TEST_F(PeerConnectionTracerTest, FiresOnClose) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
pc->pc()->Close();
EXPECT_EQ(Counts(*pc).close, 1);
EXPECT_GE(Counts(*pc).signaling_state_changed, 1);
// Calling Close again should be a no-op for the tracer.
pc->pc()->Close();
EXPECT_EQ(Counts(*pc).close, 1);
}
// The implicit SetLocalDescription overload (no SDP arg) should fire
// OnSetLocalDescription with a null description; the SDP the PeerConnection
// generated is reported on success instead.
TEST_F(PeerConnectionTracerTest, FiresOnSetLocalDescriptionImplicit) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
pc->AddAudioTrack("a");
auto observer = make_ref_counted<MockSetSessionDescriptionObserver>();
pc->pc()->SetLocalDescription(observer.get());
EXPECT_EQ(Counts(*pc).set_local_description, 1);
EXPECT_EQ(Counts(*pc).set_local_description_implicit, 1);
EXPECT_TRUE(WaitUntil([&] { return observer->called(); }));
EXPECT_EQ(Counts(*pc).set_local_description_success, 1);
EXPECT_EQ(Counts(*pc).set_local_description_success_with_sdp, 1);
}
// OnSetRemoteDescription should fire when the application calls
// SetRemoteDescription, not when the operation reaches the front of the chain.
TEST_F(PeerConnectionTracerTest, FiresOnSetRemoteDescription) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
auto callee = CreatePeerConnection();
ASSERT_TRUE(callee);
caller->AddAudioTrack("a");
auto offer = caller->CreateOffer();
ASSERT_TRUE(offer);
// Keep the callee's operations chain busy.
auto create_observer =
make_ref_counted<MockCreateSessionDescriptionObserver>();
callee->pc()->CreateOffer(create_observer.get(),
PeerConnectionInterface::RTCOfferAnswerOptions());
ASSERT_FALSE(create_observer->called());
auto set_observer = make_ref_counted<MockSetSessionDescriptionObserver>();
callee->pc()->SetRemoteDescription(set_observer.get(), offer.release());
EXPECT_EQ(Counts(*callee).set_remote_description, 1);
EXPECT_EQ(Counts(*callee).set_remote_description_success, 0);
EXPECT_TRUE(WaitUntil([&] { return set_observer->called(); }));
EXPECT_EQ(Counts(*callee).set_remote_description_success, 1);
EXPECT_EQ(Counts(*callee).set_remote_description_failure, 0);
}
// AddIceCandidate is traced as a call plus a separate outcome, so a candidate
// that is rejected still shows up as having been added.
TEST_F(PeerConnectionTracerTest, FiresOnAddIceCandidateFailure) {
auto pc = CreatePeerConnection();
ASSERT_TRUE(pc);
// No remote description, so the candidate cannot be accepted.
std::unique_ptr<IceCandidate> candidate(CreateIceCandidate(
"0", 0, "candidate:a0+B/1 1 udp 2130706432 192.168.1.1 1234 typ host",
nullptr));
ASSERT_TRUE(candidate);
std::optional<RTCError> result;
pc->pc()->AddIceCandidate(std::move(candidate),
[&](RTCError error) { result = error; });
EXPECT_EQ(Counts(*pc).add_ice_candidate, 1);
EXPECT_TRUE(WaitUntil([&] { return result.has_value(); }));
EXPECT_FALSE(result->ok());
EXPECT_EQ(Counts(*pc).add_ice_candidate_success, 0);
EXPECT_EQ(Counts(*pc).add_ice_candidate_failure, 1);
}
// AddTransceiver fires OnAddTransceiver with the init it was called with.
TEST_F(PeerConnectionTracerTest, FiresOnAddTransceiver) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
RtpTransceiverInit init;
init.direction = RtpTransceiverDirection::kRecvOnly;
ASSERT_TRUE(caller->pc()->AddTransceiver(MediaType::AUDIO, init).ok());
EXPECT_EQ(Counts(*caller).add_transceiver, 1);
EXPECT_EQ(Counts(*caller).add_transceiver_direction,
RtpTransceiverDirection::kRecvOnly);
}
// The recvonly transceiver that offerToReceive creates is not an application
// call and is not traced.
TEST_F(PeerConnectionTracerTest, DoesNotFireOnAddTransceiverForOfferToReceive) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
PeerConnectionInterface::RTCOfferAnswerOptions options;
options.offer_to_receive_audio = 1;
ASSERT_TRUE(caller->CreateOffer(options));
EXPECT_EQ(Counts(*caller).add_transceiver, 0);
}
// AddTrack fires OnAddTrack with the stream ids it was called with.
TEST_F(PeerConnectionTracerTest, FiresOnAddTrack) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
auto sender = caller->AddAudioTrack("a", {"s"});
ASSERT_TRUE(sender);
EXPECT_EQ(Counts(*caller).add_track, 1);
EXPECT_EQ(Counts(*caller).add_track_stream_ids,
std::vector<std::string>{"s"});
}
// A rejected AddTrack is not traced.
TEST_F(PeerConnectionTracerTest, DoesNotFireOnAddTrackOnFailure) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
auto sender = caller->AddAudioTrack("a");
ASSERT_TRUE(sender);
EXPECT_EQ(Counts(*caller).add_track, 1);
// Adding the same track a second time fails.
EXPECT_FALSE(caller->pc()->AddTrack(sender->track(), {}).ok());
EXPECT_EQ(Counts(*caller).add_track, 1);
}
// SetRemoteDescription with a receiving section fires OnTrack, on the
// receiving side only.
TEST_F(PeerConnectionTracerTest, FiresOnTrack) {
auto caller = CreatePeerConnection();
ASSERT_TRUE(caller);
auto callee = CreatePeerConnection();
ASSERT_TRUE(callee);
caller->AddAudioTrack("a");
auto offer = caller->CreateOffer();
ASSERT_TRUE(offer);
auto set_observer = make_ref_counted<MockSetSessionDescriptionObserver>();
callee->pc()->SetRemoteDescription(set_observer.get(), offer.release());
EXPECT_TRUE(WaitUntil([&] { return set_observer->called(); }));
EXPECT_EQ(Counts(*callee).track, 1);
EXPECT_EQ(Counts(*caller).track, 0);
}
} // namespace
} // namespace webrtc