srs/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp
OSSRS-AI 6e93dd73b5
AI: WebRTC: Support optional msid attribute per RFC 8830. v7.0.126 (#4570) (#4572)
Fix issue #4570 by supporting optional `msid` attribute in WebRTC SDP
negotiation, enabling compatibility with libdatachannel and other
clients that don't include msid information.

SRS failed to negotiate WebRTC connections from libdatachannel clients
because:
- libdatachannel SDP lacks `a=ssrc:XX msid:stream_id track_id`
attributes
- SRS required msid information to create track descriptions
- According to RFC 8830, the msid attribute and its appdata (track_id)
are **optional**

If diligently look at the SDP generated by libdatachannel:

```
a=ssrc:42 cname:video-send
a=ssrc:43 cname:audio-send
```

It's deliberately missing the `a=ssrc:XX msid:stream_id track_id` line,
comparing that with this one:

```
a=ssrc:42 cname:video-send
a=ssrc:42 msid:stream_id video_track_id
a=ssrc:43 cname:audio-send  
a=ssrc:43 msid:stream_id audio_track_id
```

In such a situation, to keep compatible with libdatachannel, if no msid
line in sdp, SRS comprehensively and consistently uses:

* app/stream as stream_id, such as live/livestream
* type=video|audio, cname, and ssrc as track_id, such as
track-video-video-send-43
2025-11-11 10:22:31 -05:00

1221 lines
56 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>
// 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, WorkflowRtcManuallyVerifyForPlayer)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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_with_h264();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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, WorkflowRtcManuallyVerifyForPublisher)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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_with_h264();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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();
}
// This test is used to verify the libdatachannel SDP offer from issue 4570.
// The issue reports that SRS returns an incomplete SDP answer when receiving
// an offer from libdatachannel library.
VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForLibdatachannel)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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-libdatachannel-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 with libdatachannel SDP offer from issue #4570
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_libdatachannel_publisher_offer_with_h264();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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->video_ssrc_) != conn->publishers_ssrc_map_.end());
EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_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 video media desc (libdatachannel puts video first)
SrsMediaDesc *video_desc = &local_sdp.media_descs_[0];
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);
// Second should be audio media desc
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[1];
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);
}
// 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 *video_desc = &local_sdp.media_descs_[0];
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");
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[1];
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");
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_);
}
// Stop the publisher
publisher->stop();
}
// This test is used to verify the basic workflow of the RTC connection with AV1 codec.
// 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, WorkflowRtcManuallyVerifyForPublisherWithAV1)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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-av1-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 with AV1 codec
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->vcodec_ = "av1"; // Specify AV1 codec
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_av1();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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_);
// AV1 uses payload type 45
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == 45);
// Verify the codec is AV1
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->name_ == "AV1");
// 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 with AV1
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_ == 45);
EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "AV1");
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 with AV1 payload type.
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(45); // AV1 payload type
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();
}
// This test is used to verify the basic workflow of the RTC connection with VP9 codec.
// 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, WorkflowRtcManuallyVerifyForPublisherWithVP9)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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-vp9-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 with VP9 codec
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->vcodec_ = "vp9"; // Specify VP9 codec
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_vp9();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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_);
// VP9 uses payload type 98
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == 98);
// Verify the codec is VP9
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->name_ == "VP9");
// 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 with VP9
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_ == 98);
EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "VP9");
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 with VP9 payload type.
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(98); // VP9 payload type
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();
}
// This test is used to verify the basic workflow of the RTC connection with G.711 PCMU codec.
// 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, WorkflowRtcManuallyVerifyForPublisherWithG711Pcmu)
{
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 MockProtocolUtility("192.168.1.100");
MockRtcSource *mock_rtc_source = new MockRtcSource();
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-g711-pcmu-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 with G.711 PCMU codec
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->acodec_ = "pcmu"; // Specify PCMU codec
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_g711_pcmu();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2);
}
// 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_);
// PCMU uses payload type 0
EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == 0);
// Verify the codec is PCMU
EXPECT_TRUE(stream_desc->audio_track_desc_->media_->name_ == "PCMU");
// 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 with PCMU
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_ == 0);
EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "PCMU");
EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 8000);
// 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 with PCMU payload type.
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(0); // PCMU payload type
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();
}