srs/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp
2025-10-23 09:44:28 -04:00

463 lines
20 KiB
C++

/**
* The MIT License (MIT)
*
* Copyright (c) 2013-2025 Winlin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <srs_utest_workflow_rtc_conn.hpp>
#include <srs_app_factory.hpp>
#include <srs_app_rtc_conn.hpp>
#include <srs_app_rtc_server.hpp>
#include <srs_app_rtc_source.hpp>
#include <srs_app_stream_token.hpp>
#include <srs_kernel_error.hpp>
#include <srs_protocol_rtmp_stack.hpp>
#include <srs_utest_ai11.hpp>
#include <srs_utest_manual_mock.hpp>
#include <srs_utest_manual_service.hpp>
MockProtocolUtilityForRtcConn::MockProtocolUtilityForRtcConn(std::string ip)
{
mock_ip_ = ip;
}
MockProtocolUtilityForRtcConn::~MockProtocolUtilityForRtcConn()
{
}
std::vector<SrsIPAddress *> &MockProtocolUtilityForRtcConn::local_ips()
{
if (!ips_.empty()) {
return ips_;
}
SrsIPAddress *addr = new SrsIPAddress();
addr->ip_ = mock_ip_;
addr->is_ipv4_ = true;
addr->is_loopback_ = false; // Not loopback
addr->is_internet_ = true; // Public IP
addr->ifname_ = "eth0"; // Interface name
ips_.push_back(addr);
return ips_;
}
MockAppFactoryForRtcConn::MockAppFactoryForRtcConn()
{
mock_protocol_utility_ = NULL;
}
MockAppFactoryForRtcConn::~MockAppFactoryForRtcConn()
{
}
ISrsProtocolUtility *MockAppFactoryForRtcConn::create_protocol_utility()
{
return mock_protocol_utility_;
}
ISrsRtcPublishStream *MockAppFactoryForRtcConn::create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid)
{
SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid);
publisher->rtc_sources_ = rtc_sources_;
return publisher;
}
MockRtcSourceForRtcConn::MockRtcSourceForRtcConn()
{
rtp_audio_count_ = 0;
rtp_video_count_ = 0;
}
MockRtcSourceForRtcConn::~MockRtcSourceForRtcConn()
{
}
srs_error_t MockRtcSourceForRtcConn::on_rtp(SrsRtpPacket *pkt)
{
if (pkt->frame_type_ == SrsFrameTypeAudio) {
rtp_audio_count_++;
} else if (pkt->frame_type_ == SrsFrameTypeVideo) {
rtp_video_count_++;
}
return srs_success;
}
// This test is used to verify the basic workflow of the RTC connection.
// It's finished with the help of AI, but each step is manually designed
// and verified. So this is not dominated by AI, but by humanbeing.
VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer)
{
srs_error_t err;
// Create mock dependencies FIRST (they must outlive the connection)
SrsUniquePtr<MockCircuitBreaker> mock_circuit_breaker(new MockCircuitBreaker());
SrsUniquePtr<MockConnectionManager> mock_conn_manager(new MockConnectionManager());
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
SrsUniquePtr<MockAppConfig> mock_config(new MockAppConfig());
SrsUniquePtr<MockDtlsCertificate> mock_dtls_certificate(new MockDtlsCertificate());
SrsUniquePtr<MockSdpFactory> mock_sdp_factory(new MockSdpFactory());
SrsUniquePtr<MockAppFactoryForRtcConn> mock_app_factory(new MockAppFactoryForRtcConn());
mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100");
MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn();
mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_
MockRtcAsyncTaskExecutor mock_exec;
SrsContextId cid;
cid.set_value("test-rtc-conn-player-workflow");
SrsUniquePtr<ISrsRtcConnection> conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
SrsRtcConnection *conn = dynamic_cast<SrsRtcConnection *>(conn_ptr.get());
EXPECT_TRUE(conn != NULL);
// Mock the RTC conn, also mock the config in publisher_negotiator_ and player_negotiator_
conn->circuit_breaker_ = mock_circuit_breaker.get();
conn->conn_manager_ = mock_conn_manager.get();
conn->rtc_sources_ = mock_rtc_sources.get();
conn->config_ = mock_config.get();
conn->dtls_certificate_ = mock_dtls_certificate.get();
conn->app_factory_ = mock_app_factory.get();
SrsRtcPublisherNegotiator *pub_neg = dynamic_cast<SrsRtcPublisherNegotiator *>(conn->publisher_negotiator_);
pub_neg->config_ = mock_config.get();
SrsRtcPlayerNegotiator *play_neg = dynamic_cast<SrsRtcPlayerNegotiator *>(conn->player_negotiator_);
play_neg->config_ = mock_config.get();
play_neg->rtc_sources_ = mock_rtc_sources.get();
// Create RTC user config for add_player
SrsUniquePtr<SrsRtcUserConfig> ruc(new SrsRtcUserConfig());
if (true) {
srs_freep(ruc->req_);
ruc->req_ = new MockRtcAsyncCallRequest("test.vhost", "live", "stream1");
ruc->publish_ = false;
ruc->dtls_ = true;
ruc->srtp_ = true;
ruc->audio_before_video_ = false;
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
}
// Add player, which negotiate the SDP and generate local SDP
SrsSdp local_sdp;
local_sdp.session_config_.dtls_role_ = mock_config->get_rtc_dtls_role(ruc->req_->vhost_);
if (true) {
HELPER_EXPECT_SUCCESS(conn->add_player(ruc.get(), local_sdp));
// Verify publishers and SSRC mappings
EXPECT_TRUE(conn->players_.size() == 1);
EXPECT_TRUE(conn->players_ssrc_map_.size() == 2);
// Verify the local SDP was generated with media information
EXPECT_TRUE(local_sdp.version_ == "0");
EXPECT_TRUE(local_sdp.group_policy_ == "BUNDLE");
EXPECT_TRUE(local_sdp.msids_.size() == 1);
EXPECT_TRUE(local_sdp.msids_[0] == "live/stream1");
EXPECT_TRUE(local_sdp.media_descs_.size() == 2);
// First should be audio media desc
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
EXPECT_TRUE(audio_desc->type_ == "audio");
EXPECT_FALSE(audio_desc->recvonly_);
EXPECT_TRUE(audio_desc->payload_types_.size() == 1);
EXPECT_TRUE(audio_desc->payload_types_[0].payload_type_ == mock_sdp_factory->audio_pt_);
EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "opus");
EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 48000);
// Second should be video media desc
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
EXPECT_TRUE(video_desc->type_ == "video");
EXPECT_FALSE(video_desc->recvonly_);
EXPECT_TRUE(video_desc->payload_types_.size() == 1);
EXPECT_TRUE(video_desc->payload_types_[0].payload_type_ == mock_sdp_factory->video_pt_);
EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "H264");
EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000);
}
// Generate local SDP and setup SDP.
std::string username;
if (true) {
bool status = true;
conn->set_all_tracks_status(ruc->req_->get_stream_url(), ruc->publish_, status);
HELPER_EXPECT_SUCCESS(conn->generate_local_sdp(ruc.get(), local_sdp, username));
conn->set_remote_sdp(ruc->remote_sdp_);
conn->set_local_sdp(local_sdp);
conn->set_state_as_waiting_stun();
// Verify the local SDP was generated ice pwd
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
EXPECT_TRUE(!audio_desc->session_info_.ice_pwd_.empty());
EXPECT_TRUE(!audio_desc->session_info_.fingerprint_.empty());
EXPECT_TRUE(audio_desc->candidates_.size() == 1);
EXPECT_TRUE(audio_desc->candidates_[0].ip_ == "192.168.1.100");
EXPECT_TRUE(audio_desc->session_info_.setup_ == "passive");
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
EXPECT_TRUE(!video_desc->session_info_.ice_pwd_.empty());
EXPECT_TRUE(!video_desc->session_info_.fingerprint_.empty());
EXPECT_TRUE(video_desc->candidates_.size() == 1);
EXPECT_TRUE(video_desc->candidates_[0].ip_ == "192.168.1.100");
EXPECT_TRUE(video_desc->session_info_.setup_ == "passive");
EXPECT_TRUE(local_sdp.session_negotiate_.dtls_role_ == "passive");
}
// Initialize the connection
if (true) {
HELPER_EXPECT_SUCCESS(conn->initialize(ruc->req_, ruc->dtls_, ruc->srtp_, username));
EXPECT_TRUE(conn->nack_enabled_);
}
// DTLS done, start player consumer
if (true) {
HELPER_EXPECT_SUCCESS(conn->on_dtls_handshake_done());
// Wait for coroutine to start. Normally it should be ready and stopped at wait for
// RTP packets from consumer.
srs_usleep(1 * SRS_UTIME_MILLISECONDS);
// Verify the consumer is created and started
EXPECT_TRUE(conn->players_.size() == 1);
SrsRtcPlayStream *player = dynamic_cast<SrsRtcPlayStream *>(conn->players_.begin()->second);
EXPECT_TRUE(player->is_started_);
// Stop the player
player->stop();
}
}
// This test is used to verify the basic workflow of the RTC connection.
// It's finished with the help of AI, but each step is manually designed
// and verified. So this is not dominated by AI, but by humanbeing.
VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
{
srs_error_t err;
// Create mock dependencies FIRST (they must outlive the connection)
SrsUniquePtr<MockCircuitBreaker> mock_circuit_breaker(new MockCircuitBreaker());
SrsUniquePtr<MockConnectionManager> mock_conn_manager(new MockConnectionManager());
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
SrsUniquePtr<MockAppConfig> mock_config(new MockAppConfig());
SrsUniquePtr<MockDtlsCertificate> mock_dtls_certificate(new MockDtlsCertificate());
SrsUniquePtr<MockSdpFactory> mock_sdp_factory(new MockSdpFactory());
SrsUniquePtr<MockAppFactoryForRtcConn> mock_app_factory(new MockAppFactoryForRtcConn());
SrsStreamPublishTokenManager token_manager;
mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100");
MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn();
mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_
MockRtcAsyncTaskExecutor mock_exec;
SrsContextId cid;
cid.set_value("test-rtc-conn-publisher-workflow");
SrsUniquePtr<ISrsRtcConnection> conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
SrsRtcConnection *conn = dynamic_cast<SrsRtcConnection *>(conn_ptr.get());
EXPECT_TRUE(conn != NULL);
// Mock the RTC conn, also mock the config in publisher_negotiator_ and player_negotiator_
conn->circuit_breaker_ = mock_circuit_breaker.get();
conn->conn_manager_ = mock_conn_manager.get();
conn->rtc_sources_ = mock_rtc_sources.get();
conn->config_ = mock_config.get();
conn->dtls_certificate_ = mock_dtls_certificate.get();
conn->app_factory_ = mock_app_factory.get();
SrsRtcPublisherNegotiator *pub_neg = dynamic_cast<SrsRtcPublisherNegotiator *>(conn->publisher_negotiator_);
pub_neg->config_ = mock_config.get();
SrsRtcPlayerNegotiator *play_neg = dynamic_cast<SrsRtcPlayerNegotiator *>(conn->player_negotiator_);
play_neg->config_ = mock_config.get();
play_neg->rtc_sources_ = mock_rtc_sources.get();
// Create RTC user config for add_publisher
SrsUniquePtr<SrsRtcUserConfig> ruc(new SrsRtcUserConfig());
if (true) {
srs_freep(ruc->req_);
ruc->req_ = new MockRtcAsyncCallRequest("test.vhost", "live", "stream1");
ruc->publish_ = true;
ruc->dtls_ = true;
ruc->srtp_ = true;
ruc->audio_before_video_ = false;
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
}
// Add publisher, which negotiate the SDP and generate local SDP
SrsSdp local_sdp;
local_sdp.session_config_.dtls_role_ = mock_config->get_rtc_dtls_role(ruc->req_->vhost_);
if (true) {
HELPER_EXPECT_SUCCESS(conn->add_publisher(ruc.get(), local_sdp));
// Verify publishers and SSRC mappings
EXPECT_TRUE(conn->publishers_.size() == 1);
EXPECT_TRUE(conn->publishers_ssrc_map_.size() == 2);
EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_ssrc_) != conn->publishers_ssrc_map_.end());
EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->video_ssrc_) != conn->publishers_ssrc_map_.end());
// Verify the source stream desription, should have two tracks.
SrsRtcSourceDescription *stream_desc = mock_rtc_sources->mock_source_->stream_desc_;
EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL);
EXPECT_TRUE(stream_desc->video_track_descs_.size() == 1);
// Verify the audio track ssrc and payload type.
EXPECT_TRUE(stream_desc->audio_track_desc_->ssrc_ == mock_sdp_factory->audio_ssrc_);
EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == mock_sdp_factory->audio_pt_);
// Verify the video track ssrc and payload type.
EXPECT_TRUE(stream_desc->video_track_descs_[0]->ssrc_ == mock_sdp_factory->video_ssrc_);
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == mock_sdp_factory->video_pt_);
// Verify the local SDP was generated with media information
EXPECT_TRUE(local_sdp.version_ == "0");
EXPECT_TRUE(local_sdp.group_policy_ == "BUNDLE");
EXPECT_TRUE(local_sdp.msids_.size() == 1);
EXPECT_TRUE(local_sdp.msids_[0] == "live/stream1");
EXPECT_TRUE(local_sdp.media_descs_.size() == 2);
// First should be audio media desc
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
EXPECT_TRUE(audio_desc->type_ == "audio");
EXPECT_TRUE(audio_desc->recvonly_);
EXPECT_TRUE(audio_desc->payload_types_.size() == 1);
EXPECT_TRUE(audio_desc->payload_types_[0].payload_type_ == mock_sdp_factory->audio_pt_);
EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "opus");
EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 48000);
// Second should be video media desc
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
EXPECT_TRUE(video_desc->type_ == "video");
EXPECT_TRUE(video_desc->recvonly_);
EXPECT_TRUE(video_desc->payload_types_.size() == 1);
EXPECT_TRUE(video_desc->payload_types_[0].payload_type_ == mock_sdp_factory->video_pt_);
EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "H264");
EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000);
}
// Generate local SDP and setup SDP.
std::string username;
if (true) {
bool status = true;
conn->set_all_tracks_status(ruc->req_->get_stream_url(), ruc->publish_, status);
HELPER_EXPECT_SUCCESS(conn->generate_local_sdp(ruc.get(), local_sdp, username));
conn->set_remote_sdp(ruc->remote_sdp_);
conn->set_local_sdp(local_sdp);
conn->set_state_as_waiting_stun();
// Verify the local SDP was generated ice pwd
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
EXPECT_TRUE(!audio_desc->session_info_.ice_pwd_.empty());
EXPECT_TRUE(!audio_desc->session_info_.fingerprint_.empty());
EXPECT_TRUE(audio_desc->candidates_.size() == 1);
EXPECT_TRUE(audio_desc->candidates_[0].ip_ == "192.168.1.100");
EXPECT_TRUE(audio_desc->session_info_.setup_ == "passive");
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
EXPECT_TRUE(!video_desc->session_info_.ice_pwd_.empty());
EXPECT_TRUE(!video_desc->session_info_.fingerprint_.empty());
EXPECT_TRUE(video_desc->candidates_.size() == 1);
EXPECT_TRUE(video_desc->candidates_[0].ip_ == "192.168.1.100");
EXPECT_TRUE(video_desc->session_info_.setup_ == "passive");
EXPECT_TRUE(local_sdp.session_negotiate_.dtls_role_ == "passive");
}
// Initialize the connection
if (true) {
HELPER_EXPECT_SUCCESS(conn->initialize(ruc->req_, ruc->dtls_, ruc->srtp_, username));
EXPECT_TRUE(conn->nack_enabled_);
// Create and set publish token
SrsStreamPublishToken *publish_token_raw = NULL;
HELPER_EXPECT_SUCCESS(token_manager.acquire_token(ruc->req_, publish_token_raw));
SrsSharedPtr<ISrsStreamPublishToken> publish_token(publish_token_raw);
conn->set_publish_token(publish_token);
EXPECT_TRUE(conn->publish_token_->is_acquired());
}
// DTLS done, start publisher
SrsRtcPublishStream *publisher = NULL;
if (true) {
HELPER_EXPECT_SUCCESS(conn->on_dtls_handshake_done());
// Wait for coroutine to start. Normally it should be ready wait for PLI requests.
srs_usleep(1 * SRS_UTIME_MILLISECONDS);
// Verify the publisher is created and started
EXPECT_TRUE(conn->publishers_.size() == 1);
publisher = dynamic_cast<SrsRtcPublishStream *>(conn->publishers_.begin()->second);
EXPECT_TRUE(publisher->is_sender_started_);
}
// Got a RTP audio packet.
for (int i = 0; i < 3; i++) {
SrsRtpPacket pkt;
pkt.header_.set_ssrc(mock_sdp_factory->audio_ssrc_);
pkt.header_.set_sequence(100);
pkt.header_.set_timestamp(1000);
pkt.header_.set_payload_type(mock_sdp_factory->audio_pt_);
SrsUniquePtr<char[]> data(new char[1500]);
SrsBuffer buf(data.get(), 1500);
HELPER_EXPECT_SUCCESS(pkt.encode(&buf));
HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos()));
HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos()));
EXPECT_EQ(mock_rtc_source->rtp_audio_count_, i + 1);
}
// Got a RTP video packet.
for (int i = 0; i < 3; i++) {
SrsRtpPacket pkt;
pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_);
pkt.header_.set_sequence(100);
pkt.header_.set_timestamp(1000);
pkt.header_.set_payload_type(mock_sdp_factory->video_pt_);
SrsUniquePtr<char[]> data(new char[1500]);
SrsBuffer buf(data.get(), 1500);
HELPER_EXPECT_SUCCESS(pkt.encode(&buf));
HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos()));
HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos()));
EXPECT_EQ(mock_rtc_source->rtp_video_count_, i + 1);
}
// Stop the publisher
publisher->stop();
}