AI: WebRTC: Support VP9 codec for WebRTC-to-WebRTC streaming. v7.0.123 (#4548) (#4565)

VP9 is a similar codec to HEVC, but for WebRTC, VP9 works better than
AVC/HEVC in some special cases. However, SRS only support VP9 for
WebRTC, doesn't support converting it to RTMP, for RTMP only support
AVC/HEVC/AV1 and SRS cannot support transcoding.

Usage:
* Publish with VP9:
[http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream&codec=vp9](http://localhost:8080/players/whip.html?codec=vp9)
* Play with VP9:
[http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream&codec=vp9](http://localhost:8080/players/whep.html?codec=vp9)
This commit is contained in:
OSSRS-AI 2025-11-08 12:47:31 -05:00 committed by GitHub
parent 1a96abc880
commit 7fcd406a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 761 additions and 175 deletions

View File

@ -439,6 +439,37 @@ The streams:
* HTTP-FLV[http://localhost:8080/live/show.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=show.flv) * HTTP-FLV[http://localhost:8080/live/show.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=show.flv)
* RTMP by VLCrtmp://localhost/live/show * RTMP by VLCrtmp://localhost/live/show
## AV1 Codec Support
SRS supports AV1 codec for WebRTC-to-WebRTC streaming since v4.0.91 ([#2324](https://github.com/ossrs/srs/pull/2324)).
AV1 is a royalty-free codec that saves 30-50% bandwidth compared to H.264. SRS implements AV1 as relay-only (SFU mode),
accepting AV1 streams via WHIP and forwarding to WHEP players without transcoding. AV1 streams cannot be converted to
RTMP/HLS or recorded to DVR.
To use AV1, add the `codec=av1` query parameter to WHIP/WHEP URLs:
* Publish: `http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream&codec=av1`
* Play: `http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream&codec=av1`
Browser support: Chrome/Edge M90+, Firefox (full support), Safari (decode only). Use H.264 if you need RTMP/HLS conversion,
DVR recording, or maximum compatibility.
## VP9 Codec Support
SRS supports VP9 codec for WebRTC-to-WebRTC streaming since v7.0.0 ([#4548](https://github.com/ossrs/srs/issues/4548)).
VP9 is a royalty-free codec that saves 20-40% bandwidth compared to H.264. VP9 works better than H.264/H.265 with congestion control
in WebRTC, making it ideal for keeping streams live under network fluctuations. SRS implements VP9 as relay-only (SFU mode),
accepting VP9 streams via WHIP and forwarding to WHEP players without transcoding. VP9 streams cannot be converted to
RTMP/HLS or recorded to DVR.
To use VP9, add the `codec=vp9` query parameter to WHIP/WHEP URLs:
* Publish: `http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream&codec=vp9`
* Play: `http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream&codec=vp9`
Browser support: Chrome/Edge M29+, Firefox M28+, Opera M16+. Safari does not support VP9. Use H.264 if you need RTMP/HLS conversion,
DVR recording, or Safari compatibility.
## SFU: One to One ## SFU: One to One
Please use `conf/rtc.conf` as config. Please use `conf/rtc.conf` as config.

View File

@ -7,6 +7,7 @@ The changelog for SRS.
<a name="v7-changes"></a> <a name="v7-changes"></a>
## SRS 7.0 Changelog ## SRS 7.0 Changelog
* v7.0, 2025-11-08, AI: WebRTC: Support VP9 codec for WebRTC-to-WebRTC streaming. v7.0.123 (#4548)
* v7.0, 2025-11-08, AI: API: Add audio_frames and video_frames to HTTP API. v7.0.122 (#4559) * v7.0, 2025-11-08, AI: API: Add audio_frames and video_frames to HTTP API. v7.0.122 (#4559)
* v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502) * v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502)
* v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426) * v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426)

View File

@ -666,7 +666,7 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
string code_str; string code_str;
if (true) { if (true) {
srs_error_t err = srs_success; srs_error_t err = srs_success;
err = serve_http_with(w, r); err = serve_http_with(w, r);
if (err == srs_success) { if (err == srs_success) {
return err; return err;
@ -674,8 +674,8 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
code = srs_error_code(err); code = srs_error_code(err);
code_str = srs_error_code_str(err); code_str = srs_error_code_str(err);
srs_warn("WHIP: serve http for %s with err %d:%s, %s", srs_warn("WHIP: serve http for %s with err %d:%s, %s",
r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str()); r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str());
srs_freep(err); srs_freep(err);
} }

View File

@ -3402,6 +3402,38 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo
} }
} }
track_desc->type_ = "video";
track_desc->set_codec_payload((SrsCodecPayload *)video_payload);
break;
}
} else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdVP9) {
std::vector<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("VP9");
if (payloads.empty()) {
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid VP9 payload type");
}
for (int j = 0; j < (int)payloads.size(); j++) {
const SrsMediaPayloadType &payload = payloads.at(j);
// Generate video payload for vp9.
SrsVideoPayload *video_payload = new SrsVideoPayload(payload.payload_type_, payload.encoding_name_, payload.clock_rate_);
// TODO: FIXME: Only support some transport algorithms.
for (int k = 0; k < (int)payload.rtcp_fb_.size(); ++k) {
const string &rtcp_fb = payload.rtcp_fb_.at(k);
if (nack_enabled) {
if (rtcp_fb == "nack" || rtcp_fb == "nack pli") {
video_payload->rtcp_fbs_.push_back(rtcp_fb);
}
}
if (twcc_enabled && remote_twcc_id) {
if (rtcp_fb == "transport-cc") {
video_payload->rtcp_fbs_.push_back(rtcp_fb);
}
}
}
track_desc->type_ = "video"; track_desc->type_ = "video";
track_desc->set_codec_payload((SrsCodecPayload *)video_payload); track_desc->set_codec_payload((SrsCodecPayload *)video_payload);
break; break;
@ -3804,6 +3836,14 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig *
// @see https://bugs.chromium.org/p/webrtc/issues/detail?id=13166 // @see https://bugs.chromium.org/p/webrtc/issues/detail?id=13166
track_descs = source->get_track_desc("video", "AV1X"); track_descs = source->get_track_desc("video", "AV1X");
} }
} else if (prefer_codec == SrsVideoCodecIdVP9) {
std::vector<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("VP9");
if (payloads.empty()) {
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid VP9 payload type");
}
remote_payload = payloads.at(0);
track_descs = source->get_track_desc("video", "VP9");
} else if (prefer_codec == SrsVideoCodecIdHEVC) { } else if (prefer_codec == SrsVideoCodecIdHEVC) {
std::vector<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("H265"); std::vector<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("H265");
if (payloads.empty()) { if (payloads.empty()) {

View File

@ -3851,7 +3851,7 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio)
SrsAudioChannels channels = (SrsAudioChannels)audio_media->channel_; SrsAudioChannels channels = (SrsAudioChannels)audio_media->channel_;
if ((err = stat_->on_audio_info(req_, codec_id, sample_rate, channels, if ((err = stat_->on_audio_info(req_, codec_id, sample_rate, channels,
SrsAacObjectTypeReserved)) != srs_success) { SrsAacObjectTypeReserved)) != srs_success) {
return srs_error_wrap(err, "stat audio info"); return srs_error_wrap(err, "stat audio info");
} }
srs_trace("RTC: parsed %s codec, sample_rate=%dHz, channels=%d", srs_trace("RTC: parsed %s codec, sample_rate=%dHz, channels=%d",
@ -3926,6 +3926,13 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio)
srs_video_codec_id2str(codec_id).c_str(), srs_video_codec_id2str(codec_id).c_str(),
srs_hevc_profile2str(profile).c_str(), srs_hevc_profile2str(profile).c_str(),
level_id); level_id);
} else if (codec_id == SrsVideoCodecIdAV1 || codec_id == SrsVideoCodecIdVP9) {
// AV1 and VP9 are relay-only codecs for WebRTC-to-WebRTC
// No detailed profile/level parsing needed for relay mode
if ((err = stat_->on_video_info(req_, codec_id, 0, 0, 0, 0)) != srs_success) {
return srs_error_wrap(err, "stat video info");
}
srs_trace("RTC: parsed %s codec", srs_video_codec_id2str(codec_id).c_str());
} }
} }

View File

@ -154,6 +154,12 @@ srs_error_t SrsStatisticStream::dumps(SrsJsonObject *obj)
} else if (vcodec_ == SrsVideoCodecIdHEVC) { } else if (vcodec_ == SrsVideoCodecIdHEVC) {
video->set("profile", SrsJsonAny::str(srs_hevc_profile2str(hevc_profile_).c_str())); video->set("profile", SrsJsonAny::str(srs_hevc_profile2str(hevc_profile_).c_str()));
video->set("level", SrsJsonAny::str(srs_hevc_level2str(hevc_level_).c_str())); video->set("level", SrsJsonAny::str(srs_hevc_level2str(hevc_level_).c_str()));
} else if (vcodec_ == SrsVideoCodecIdAV1) {
video->set("profile", SrsJsonAny::null());
video->set("level", SrsJsonAny::null());
} else if (vcodec_ == SrsVideoCodecIdVP9) {
video->set("profile", SrsJsonAny::null());
video->set("level", SrsJsonAny::null());
} else { } else {
video->set("profile", SrsJsonAny::str("Other")); video->set("profile", SrsJsonAny::str("Other"));
video->set("level", SrsJsonAny::str("Other")); video->set("level", SrsJsonAny::str("Other"));

View File

@ -9,6 +9,6 @@
#define VERSION_MAJOR 7 #define VERSION_MAJOR 7
#define VERSION_MINOR 0 #define VERSION_MINOR 0
#define VERSION_REVISION 122 #define VERSION_REVISION 123
#endif #endif

View File

@ -82,6 +82,8 @@ string srs_video_codec_id2str(SrsVideoCodecId codec)
return "HEVC"; return "HEVC";
case SrsVideoCodecIdAV1: case SrsVideoCodecIdAV1:
return "AV1"; return "AV1";
case SrsVideoCodecIdVP9:
return "VP9";
case SrsVideoCodecIdReserved: case SrsVideoCodecIdReserved:
case SrsVideoCodecIdReserved1: case SrsVideoCodecIdReserved1:
case SrsVideoCodecIdReserved2: case SrsVideoCodecIdReserved2:
@ -105,6 +107,8 @@ SrsVideoCodecId srs_video_codec_str2id(const std::string &codec)
return SrsVideoCodecIdHEVC; return SrsVideoCodecIdHEVC;
} else if (upper_codec == "AV1") { } else if (upper_codec == "AV1") {
return SrsVideoCodecIdAV1; return SrsVideoCodecIdAV1;
} else if (upper_codec == "VP9") {
return SrsVideoCodecIdVP9;
} else if (upper_codec == "VP6") { } else if (upper_codec == "VP6") {
return SrsVideoCodecIdOn2VP6; return SrsVideoCodecIdOn2VP6;
} else if (upper_codec == "VP6A") { } else if (upper_codec == "VP6A") {

View File

@ -61,8 +61,10 @@ enum SrsVideoCodecId {
SrsVideoCodecIdAVC = 7, SrsVideoCodecIdAVC = 7,
// See page 79 at @doc https://github.com/CDN-Union/H265/blob/master/Document/video_file_format_spec_v10_1_ksyun_20170615.doc // See page 79 at @doc https://github.com/CDN-Union/H265/blob/master/Document/video_file_format_spec_v10_1_ksyun_20170615.doc
SrsVideoCodecIdHEVC = 12, SrsVideoCodecIdHEVC = 12,
// https://mp.weixin.qq.com/s/H3qI7zsON5sdf4oDJ9qlkg // AV1 codec for WebRTC, https://github.com/ossrs/srs/pull/2324
SrsVideoCodecIdAV1 = 13, SrsVideoCodecIdAV1 = 13,
// VP9 codec for WebRTC, https://github.com/ossrs/srs/pull/4565
SrsVideoCodecIdVP9 = 14,
}; };
std::string srs_video_codec_id2str(SrsVideoCodecId codec); std::string srs_video_codec_id2str(SrsVideoCodecId codec);
SrsVideoCodecId srs_video_codec_str2id(const std::string &codec); SrsVideoCodecId srs_video_codec_str2id(const std::string &codec);

View File

@ -111,10 +111,9 @@
XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \ XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \
XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \ XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \
XX(ERROR_NOT_SUPPORTED, 1100, "NotSupported", "Feature is not supported") \ XX(ERROR_NOT_SUPPORTED, 1100, "NotSupported", "Feature is not supported") \
XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \ XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \
XX(ERROR_SYSTEM_AUTH, 1102, "SystemAuth", "Failed to authenticate stream") XX(ERROR_SYSTEM_AUTH, 1102, "SystemAuth", "Failed to authenticate stream")
/**************************************************/ /**************************************************/
/* RTMP protocol error. */ /* RTMP protocol error. */
#define SRS_ERRNO_MAP_RTMP(XX) \ #define SRS_ERRNO_MAP_RTMP(XX) \
@ -382,7 +381,7 @@
XX(ERROR_RTSP_NO_TRACK, 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \ XX(ERROR_RTSP_NO_TRACK, 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \
XX(ERROR_RTSP_TOKEN_NOT_NORMAL, 5040, "RtspToken", "Invalid RTSP token state not normal") \ XX(ERROR_RTSP_TOKEN_NOT_NORMAL, 5040, "RtspToken", "Invalid RTSP token state not normal") \
XX(ERROR_RTSP_REQUEST_HEADER_EOF, 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \ XX(ERROR_RTSP_REQUEST_HEADER_EOF, 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \
XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \ XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \
XX(ERROR_RTC_INVALID_SDP, 5043, "RtcInvalidSdp", "Invalid SDP for RTC") XX(ERROR_RTC_INVALID_SDP, 5043, "RtcInvalidSdp", "Invalid SDP for RTC")
/**************************************************/ /**************************************************/
@ -492,7 +491,7 @@ public:
// } // }
#define srs_error_new(code, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__) #define srs_error_new(code, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)
// Wrap an existing error with additional context. The error code is // Wrap an existing error with additional context. The error code is
// preserved from the wrapped error. // preserved from the wrapped error.
// //
// Example: // Example:
@ -501,8 +500,8 @@ public:
// } // }
#define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__) #define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
// Transform an error by wrapping it and changing its error code. Useful // Transform an error by wrapping it and changing its error code. Useful
// for converting internal errors to protocol-specific error codes (e.g., // for converting internal errors to protocol-specific error codes (e.g.,
// HTTP, RTMP). The wrapped error chain is preserved. // HTTP, RTMP). The wrapped error chain is preserved.
// //
// Example: // Example:

View File

@ -443,6 +443,7 @@ srs_error_t SrsTsContext::encode(ISrsStreamWriter *writer, SrsTsMessage *msg, Sr
case SrsVideoCodecIdOn2VP6WithAlphaChannel: case SrsVideoCodecIdOn2VP6WithAlphaChannel:
case SrsVideoCodecIdScreenVideoVersion2: case SrsVideoCodecIdScreenVideoVersion2:
case SrsVideoCodecIdAV1: case SrsVideoCodecIdAV1:
case SrsVideoCodecIdVP9:
vs = SrsTsStreamReserved; vs = SrsTsStreamReserved;
break; break;
} }

View File

@ -21,6 +21,7 @@ using namespace std;
#include <srs_kernel_utility.hpp> #include <srs_kernel_utility.hpp>
#include <srs_protocol_utility.hpp> #include <srs_protocol_utility.hpp>
#include <srs_utest_ai23.hpp> #include <srs_utest_ai23.hpp>
#include <srs_utest_manual_mock.hpp>
#include <sstream> #include <sstream>
// Mock ISrsSrtSocket implementation // Mock ISrsSrtSocket implementation
@ -193,10 +194,11 @@ VOID TEST(UdpListenerTest, ListenAndReceivePacket)
dest_addr.sin_port = htons(port); dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
for (int i = 0; i < 3; i++) { for (int i = 0; i < 10; i++) {
int sent = srs_sendto(client_fd, (void *)test_data.c_str(), test_data.size(), int sent = srs_sendto(client_fd, (void *)test_data.c_str(), test_data.size(),
(sockaddr *)&dest_addr, sizeof(dest_addr), SRS_UTIME_NO_TIMEOUT); (sockaddr *)&dest_addr, sizeof(dest_addr), SRS_UTIME_NO_TIMEOUT);
EXPECT_EQ(sent, (int)test_data.size()); EXPECT_EQ(sent, (int)test_data.size());
srs_usleep(1 * SRS_UTIME_MILLISECONDS);
} }
// Wait a bit for the listener to receive and process the packet // Wait a bit for the listener to receive and process the packet
@ -2186,39 +2188,8 @@ VOID TEST(ApiServerAsCandidatesTest, MajorUseScenario)
EXPECT_TRUE(candidate_ips.find("192.168.1.100") != candidate_ips.end()); EXPECT_TRUE(candidate_ips.find("192.168.1.100") != candidate_ips.end());
} }
// Mock SrsProtocolUtility implementation // Note: MockProtocolUtility has been merged into MockProtocolUtility
MockProtocolUtility::MockProtocolUtility() // in srs_utest_manual_mock.hpp/cpp. All usages below now use MockProtocolUtility.
{
}
MockProtocolUtility::~MockProtocolUtility()
{
clear_ips();
}
vector<SrsIPAddress *> &MockProtocolUtility::local_ips()
{
return mock_ips_;
}
void MockProtocolUtility::add_ip(string ip, string ifname, bool is_ipv4, bool is_loopback, bool is_internet)
{
SrsIPAddress *addr = new SrsIPAddress();
addr->ip_ = ip;
addr->ifname_ = ifname;
addr->is_ipv4_ = is_ipv4;
addr->is_loopback_ = is_loopback;
addr->is_internet_ = is_internet;
mock_ips_.push_back(addr);
}
void MockProtocolUtility::clear_ips()
{
for (size_t i = 0; i < mock_ips_.size(); i++) {
srs_freep(mock_ips_[i]);
}
mock_ips_.clear();
}
// Mock ISrsAppConfig for discover_candidates implementation // Mock ISrsAppConfig for discover_candidates implementation
MockAppConfigForDiscoverCandidates::MockAppConfigForDiscoverCandidates() MockAppConfigForDiscoverCandidates::MockAppConfigForDiscoverCandidates()

View File

@ -196,19 +196,8 @@ public:
void clear_calls(); void clear_calls();
}; };
// Mock SrsProtocolUtility for testing discover_candidates // Note: MockProtocolUtility has been merged into MockProtocolUtility
class MockProtocolUtility : public SrsProtocolUtility // in srs_utest_manual_mock.hpp. Use MockProtocolUtility instead.
{
public:
std::vector<SrsIPAddress *> mock_ips_;
public:
MockProtocolUtility();
virtual ~MockProtocolUtility();
virtual std::vector<SrsIPAddress *> &local_ips();
void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet);
void clear_ips();
};
// Mock ISrsAppConfig for testing discover_candidates // Mock ISrsAppConfig for testing discover_candidates
class MockAppConfigForDiscoverCandidates : public MockAppConfig class MockAppConfigForDiscoverCandidates : public MockAppConfig

View File

@ -56,7 +56,7 @@ MockSdpFactory::~MockSdpFactory()
{ {
} }
std::string MockSdpFactory::create_chrome_player_offer() std::string MockSdpFactory::create_chrome_player_offer_with_h264()
{ {
// Create a real Chrome-like WebRTC SDP offer for a player (subscriber) with H.264 video and Opus audio // Create a real Chrome-like WebRTC SDP offer for a player (subscriber) with H.264 video and Opus audio
// Use member variables for SSRC and payload type values // Use member variables for SSRC and payload type values
@ -104,7 +104,7 @@ std::string MockSdpFactory::create_chrome_player_offer()
return ss.str(); return ss.str();
} }
std::string MockSdpFactory::create_chrome_publisher_offer() std::string MockSdpFactory::create_chrome_publisher_offer_with_h264()
{ {
// Create a real Chrome-like WebRTC SDP offer with H.264 video and Opus audio // Create a real Chrome-like WebRTC SDP offer with H.264 video and Opus audio
// Use member variables for SSRC and payload type values // Use member variables for SSRC and payload type values
@ -155,6 +155,113 @@ std::string MockSdpFactory::create_chrome_publisher_offer()
return ss.str(); return ss.str();
} }
std::string MockSdpFactory::create_chrome_publisher_offer_with_av1()
{
// Create a real Chrome-like WebRTC SDP offer with AV1 video and Opus audio
// Use member variables for SSRC and payload type values
// AV1 payload type is typically 45 or 96+
uint8_t av1_pt = 45;
std::stringstream ss;
ss << "v=0\r\n"
<< "o=- 4611731400430051338 2 IN IP4 127.0.0.1\r\n"
<< "s=-\r\n"
<< "t=0 0\r\n"
<< "a=group:BUNDLE 0 1\r\n"
<< "a=msid-semantic: WMS stream\r\n"
// Audio media description (Opus)
<< "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)audio_pt_ << "\r\n"
<< "c=IN IP4 0.0.0.0\r\n"
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
<< "a=ice-ufrag:test1234\r\n"
<< "a=ice-pwd:testpassword1234567890\r\n"
<< "a=ice-options:trickle\r\n"
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
<< "a=setup:actpass\r\n"
<< "a=mid:0\r\n"
<< "a=sendonly\r\n"
<< "a=rtcp-mux\r\n"
<< "a=rtpmap:" << (int)audio_pt_ << " opus/48000/2\r\n"
<< "a=fmtp:" << (int)audio_pt_ << " minptime=10;useinbandfec=1\r\n"
<< "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n"
<< "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n"
// Video media description (AV1)
<< "m=video 9 UDP/TLS/RTP/SAVPF " << (int)av1_pt << "\r\n"
<< "c=IN IP4 0.0.0.0\r\n"
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
<< "a=ice-ufrag:test1234\r\n"
<< "a=ice-pwd:testpassword1234567890\r\n"
<< "a=ice-options:trickle\r\n"
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
<< "a=setup:actpass\r\n"
<< "a=mid:1\r\n"
<< "a=sendonly\r\n"
<< "a=rtcp-mux\r\n"
<< "a=rtcp-rsize\r\n"
<< "a=rtpmap:" << (int)av1_pt << " AV1/90000\r\n"
<< "a=rtcp-fb:" << (int)av1_pt << " nack\r\n"
<< "a=rtcp-fb:" << (int)av1_pt << " nack pli\r\n"
<< "a=rtcp-fb:" << (int)av1_pt << " transport-cc\r\n"
<< "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n"
<< "a=ssrc:" << video_ssrc_ << " msid:stream video\r\n";
return ss.str();
}
std::string MockSdpFactory::create_chrome_publisher_offer_with_vp9()
{
// Create a real Chrome-like WebRTC SDP offer with VP9 video and Opus audio
// Use member variables for SSRC and payload type values
// VP9 payload type is typically 98 (Profile 0) or 100 (Profile 2)
uint8_t vp9_pt = 98;
std::stringstream ss;
ss << "v=0\r\n"
<< "o=- 4611731400430051338 2 IN IP4 127.0.0.1\r\n"
<< "s=-\r\n"
<< "t=0 0\r\n"
<< "a=group:BUNDLE 0 1\r\n"
<< "a=msid-semantic: WMS stream\r\n"
// Audio media description (Opus)
<< "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)audio_pt_ << "\r\n"
<< "c=IN IP4 0.0.0.0\r\n"
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
<< "a=ice-ufrag:test1234\r\n"
<< "a=ice-pwd:testpassword1234567890\r\n"
<< "a=ice-options:trickle\r\n"
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
<< "a=setup:actpass\r\n"
<< "a=mid:0\r\n"
<< "a=sendonly\r\n"
<< "a=rtcp-mux\r\n"
<< "a=rtpmap:" << (int)audio_pt_ << " opus/48000/2\r\n"
<< "a=fmtp:" << (int)audio_pt_ << " minptime=10;useinbandfec=1\r\n"
<< "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n"
<< "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n"
// Video media description (VP9)
<< "m=video 9 UDP/TLS/RTP/SAVPF " << (int)vp9_pt << "\r\n"
<< "c=IN IP4 0.0.0.0\r\n"
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
<< "a=ice-ufrag:test1234\r\n"
<< "a=ice-pwd:testpassword1234567890\r\n"
<< "a=ice-options:trickle\r\n"
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
<< "a=setup:actpass\r\n"
<< "a=mid:1\r\n"
<< "a=sendonly\r\n"
<< "a=rtcp-mux\r\n"
<< "a=rtcp-rsize\r\n"
<< "a=rtpmap:" << (int)vp9_pt << " VP9/90000\r\n"
<< "a=rtcp-fb:" << (int)vp9_pt << " goog-remb\r\n"
<< "a=rtcp-fb:" << (int)vp9_pt << " transport-cc\r\n"
<< "a=rtcp-fb:" << (int)vp9_pt << " ccm fir\r\n"
<< "a=rtcp-fb:" << (int)vp9_pt << " nack\r\n"
<< "a=rtcp-fb:" << (int)vp9_pt << " nack pli\r\n"
<< "a=fmtp:" << (int)vp9_pt << " profile-id=0\r\n"
<< "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n"
<< "a=ssrc:" << video_ssrc_ << " msid:stream video\r\n";
return ss.str();
}
MockDtlsCertificate::MockDtlsCertificate() MockDtlsCertificate::MockDtlsCertificate()
{ {
fingerprint_ = "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"; fingerprint_ = "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99";
@ -306,6 +413,8 @@ ISrsRequest *MockRtcAsyncCallRequest::as_http()
MockRtcSource::MockRtcSource() MockRtcSource::MockRtcSource()
{ {
on_rtp_count_ = 0; on_rtp_count_ = 0;
rtp_audio_count_ = 0;
rtp_video_count_ = 0;
} }
MockRtcSource::~MockRtcSource() MockRtcSource::~MockRtcSource()
@ -315,6 +424,14 @@ MockRtcSource::~MockRtcSource()
srs_error_t MockRtcSource::on_rtp(SrsRtpPacket *pkt) srs_error_t MockRtcSource::on_rtp(SrsRtpPacket *pkt)
{ {
on_rtp_count_++; on_rtp_count_++;
// Count audio and video packets separately
if (pkt->frame_type_ == SrsFrameTypeAudio) {
rtp_audio_count_++;
} else if (pkt->frame_type_ == SrsFrameTypeVideo) {
rtp_video_count_++;
}
return SrsRtcSource::on_rtp(pkt); return SrsRtcSource::on_rtp(pkt);
} }
@ -2337,3 +2454,53 @@ void MockAudioTranscoder::aac_codec_header(uint8_t **data, int *len)
*data = copy; *data = copy;
*len = size; *len = size;
} }
// Mock ISrsProtocolUtility implementation
MockProtocolUtility::MockProtocolUtility(std::string ip)
{
mock_ip_ = ip;
}
MockProtocolUtility::~MockProtocolUtility()
{
clear_ips();
}
std::vector<SrsIPAddress *> &MockProtocolUtility::local_ips()
{
if (!ips_.empty()) {
return ips_;
}
// If mock_ip_ is set (via constructor), create a default IP address
if (!mock_ip_.empty()) {
SrsIPAddress *addr = new SrsIPAddress();
addr->ip_ = mock_ip_;
addr->is_ipv4_ = true;
addr->is_loopback_ = false; // Not loopback
addr->is_internet_ = true; // Public IP
addr->ifname_ = "eth0"; // Interface name
ips_.push_back(addr);
}
return ips_;
}
void MockProtocolUtility::add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet)
{
SrsIPAddress *addr = new SrsIPAddress();
addr->ip_ = ip;
addr->ifname_ = ifname;
addr->is_ipv4_ = is_ipv4;
addr->is_loopback_ = is_loopback;
addr->is_internet_ = is_internet;
ips_.push_back(addr);
}
void MockProtocolUtility::clear_ips()
{
for (size_t i = 0; i < ips_.size(); i++) {
srs_freep(ips_[i]);
}
ips_.clear();
}

View File

@ -78,9 +78,13 @@ public:
public: public:
// Create a Chrome-like WebRTC publisher offer SDP // Create a Chrome-like WebRTC publisher offer SDP
std::string create_chrome_publisher_offer(); std::string create_chrome_publisher_offer_with_h264();
// Create a Chrome-like WebRTC player offer SDP // Create a Chrome-like WebRTC player offer SDP
std::string create_chrome_player_offer(); std::string create_chrome_player_offer_with_h264();
// Create a Chrome-like WebRTC publisher offer SDP with AV1
std::string create_chrome_publisher_offer_with_av1();
// Create a Chrome-like WebRTC publisher offer SDP with VP9
std::string create_chrome_publisher_offer_with_vp9();
}; };
// Mock DTLS certificate for testing // Mock DTLS certificate for testing
@ -162,6 +166,8 @@ class MockRtcSource : public SrsRtcSource
{ {
public: public:
int on_rtp_count_; int on_rtp_count_;
int rtp_audio_count_;
int rtp_video_count_;
public: public:
MockRtcSource(); MockRtcSource();
@ -1332,4 +1338,23 @@ public:
virtual void aac_codec_header(uint8_t **data, int *len); virtual void aac_codec_header(uint8_t **data, int *len);
}; };
// Mock ISrsProtocolUtility for testing RTC connections
// This class merges functionality from MockProtocolUtility in srs_utest_ai18.hpp
// It supports both simple single-IP usage (via constructor) and complex multi-IP usage (via add_ip)
class MockProtocolUtility : public ISrsProtocolUtility
{
public:
std::vector<SrsIPAddress *> ips_;
std::string mock_ip_;
public:
MockProtocolUtility(std::string ip = "");
virtual ~MockProtocolUtility();
public:
virtual std::vector<SrsIPAddress *> &local_ips();
void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet);
void clear_ips();
};
#endif #endif

View File

@ -34,73 +34,6 @@
#include <srs_utest_manual_mock.hpp> #include <srs_utest_manual_mock.hpp>
#include <srs_utest_manual_service.hpp> #include <srs_utest_manual_service.hpp>
MockProtocolUtilityForRtcConn::MockProtocolUtilityForRtcConn(std::string ip)
{
mock_ip_ = ip;
}
MockProtocolUtilityForRtcConn::~MockProtocolUtilityForRtcConn()
{
}
std::vector<SrsIPAddress *> &MockProtocolUtilityForRtcConn::local_ips()
{
if (!ips_.empty()) {
return ips_;
}
SrsIPAddress *addr = new SrsIPAddress();
addr->ip_ = mock_ip_;
addr->is_ipv4_ = true;
addr->is_loopback_ = false; // Not loopback
addr->is_internet_ = true; // Public IP
addr->ifname_ = "eth0"; // Interface name
ips_.push_back(addr);
return ips_;
}
MockAppFactoryForRtcConn::MockAppFactoryForRtcConn()
{
mock_protocol_utility_ = NULL;
}
MockAppFactoryForRtcConn::~MockAppFactoryForRtcConn()
{
}
ISrsProtocolUtility *MockAppFactoryForRtcConn::create_protocol_utility()
{
return mock_protocol_utility_;
}
ISrsRtcPublishStream *MockAppFactoryForRtcConn::create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid)
{
SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid);
publisher->rtc_sources_ = rtc_sources_;
return publisher;
}
MockRtcSourceForRtcConn::MockRtcSourceForRtcConn()
{
rtp_audio_count_ = 0;
rtp_video_count_ = 0;
}
MockRtcSourceForRtcConn::~MockRtcSourceForRtcConn()
{
}
srs_error_t MockRtcSourceForRtcConn::on_rtp(SrsRtpPacket *pkt)
{
if (pkt->frame_type_ == SrsFrameTypeAudio) {
rtp_audio_count_++;
} else if (pkt->frame_type_ == SrsFrameTypeVideo) {
rtp_video_count_++;
}
return srs_success;
}
// This test is used to verify the basic workflow of the RTC connection. // 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 // 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. // and verified. So this is not dominated by AI, but by humanbeing.
@ -120,8 +53,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer)
mock_config->rtc_dtls_role_ = "passive"; mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint"; mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100"); mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100");
MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn(); MockRtcSource *mock_rtc_source = new MockRtcSource();
mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source); mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_ // Create a real ISrsRtcConnection using _srs_app_factory_
@ -157,7 +90,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer)
ruc->srtp_ = true; ruc->srtp_ = true;
ruc->audio_before_video_ = false; ruc->audio_before_video_ = false;
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer(); ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer_with_h264();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
} }
@ -271,8 +204,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
mock_config->rtc_dtls_role_ = "passive"; mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint"; mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100"); mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100");
MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn(); MockRtcSource *mock_rtc_source = new MockRtcSource();
mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source); mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_ // Create a real ISrsRtcConnection using _srs_app_factory_
@ -308,7 +241,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
ruc->srtp_ = true; ruc->srtp_ = true;
ruc->audio_before_video_ = false; ruc->audio_before_video_ = false;
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer(); ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_h264();
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
} }
@ -460,3 +393,433 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
// Stop the publisher // Stop the publisher
publisher->stop(); 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->codec_ = "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->codec_ = "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();
}

View File

@ -30,54 +30,34 @@
#include <srs_utest.hpp> #include <srs_utest.hpp>
#include <srs_app_factory.hpp> #include <srs_app_factory.hpp>
#include <srs_protocol_utility.hpp> #include <srs_utest_manual_mock.hpp>
#include <string>
#include <vector>
class MockAppFactoryForRtcConn;
class MockProtocolUtilityForRtcConn : public ISrsProtocolUtility
{
public:
std::vector<SrsIPAddress *> ips_;
std::string mock_ip_;
public:
MockProtocolUtilityForRtcConn(std::string ip);
virtual ~MockProtocolUtilityForRtcConn();
public:
virtual std::vector<SrsIPAddress *> &local_ips();
};
class MockAppFactoryForRtcConn : public SrsAppFactory class MockAppFactoryForRtcConn : public SrsAppFactory
{ {
public: public:
ISrsRtcSourceManager *rtc_sources_; ISrsRtcSourceManager *rtc_sources_;
MockProtocolUtilityForRtcConn *mock_protocol_utility_; MockProtocolUtility *mock_protocol_utility_;
public: public:
MockAppFactoryForRtcConn(); MockAppFactoryForRtcConn()
virtual ~MockAppFactoryForRtcConn(); {
mock_protocol_utility_ = NULL;
}
virtual ~MockAppFactoryForRtcConn()
{
}
public: public:
virtual ISrsProtocolUtility *create_protocol_utility(); virtual ISrsProtocolUtility *create_protocol_utility()
virtual ISrsRtcPublishStream *create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid); {
}; return mock_protocol_utility_;
}
class MockRtcSourceForRtcConn : public SrsRtcSource virtual ISrsRtcPublishStream *create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid)
{ {
public: SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid);
int rtp_audio_count_; publisher->rtc_sources_ = rtc_sources_;
int rtp_video_count_; return publisher;
}
public:
MockRtcSourceForRtcConn();
virtual ~MockRtcSourceForRtcConn();
public:
virtual srs_error_t on_rtp(SrsRtpPacket *pkt);
}; };
#endif #endif