This PR adds G.711 (PCMU/PCMA) audio codec support for WebRTC in SRS, enabling relay-only streaming of G.711 audio between WebRTC clients via WHIP/WHEP. G.711 is a widely-used, royalty-free audio codec with excellent compatibility across VoIP systems, IP cameras, and legacy telephony equipment. Fixes #4075 Many IP cameras, VoIP systems, and IoT devices use G.711 (PCMU/PCMA) as their default audio codec. Previously, SRS only supported Opus for WebRTC audio, requiring transcoding or rejecting G.711 streams entirely. This PR enables direct relay of G.711 audio streams in WebRTC, similar to how VP9/AV1 video codecs are supported. Enhanced WHIP/WHEP players with URL-based codec selection: ``` # Audio codec only http://localhost:8080/players/whip.html?acodec=pcmu http://localhost:8080/players/whip.html?acodec=pcma # Video + audio codecs http://localhost:8080/players/whip.html?vcodec=vp9&acodec=pcmu http://localhost:8080/players/whep.html?vcodec=h264&acodec=pcma # Backward compatible (codec = vcodec) http://localhost:8080/players/whip.html?codec=vp9 ``` Testing ```bash # Build and run unit tests cd trunk make utest -j && ./objs/srs_utest # Test with WHIP player # 1. Start SRS server ./objs/srs -c conf/rtc.conf # 2. Open WHIP publisher with PCMU audio http://localhost:8080/players/whip.html?acodec=pcmu # 3. Open WHEP player to receive stream http://localhost:8080/players/whep.html ``` ## Related Issues - Fixes #4075 - WebRTC G.711A Audio Codec Support - Related to #4548 - VP9 codec support (similar relay-only pattern)
1041 lines
47 KiB
C++
1041 lines
47 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, 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 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_));
|
|
}
|
|
|
|
// 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 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_));
|
|
}
|
|
|
|
// 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 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, ManuallyVerifyForPublisherWithAV1)
|
|
{
|
|
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_));
|
|
}
|
|
|
|
// 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, ManuallyVerifyForPublisherWithVP9)
|
|
{
|
|
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_));
|
|
}
|
|
|
|
// 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, ManuallyVerifyForPublisherWithG711Pcmu)
|
|
{
|
|
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_));
|
|
}
|
|
|
|
// 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();
|
|
}
|