diff --git a/trunk/3rdparty/srs-docs/doc/webrtc.md b/trunk/3rdparty/srs-docs/doc/webrtc.md
index 221489f49..318bb3075 100644
--- a/trunk/3rdparty/srs-docs/doc/webrtc.md
+++ b/trunk/3rdparty/srs-docs/doc/webrtc.md
@@ -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)
* RTMP by VLC:rtmp://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
Please use `conf/rtc.conf` as config.
diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index 4d32a25a5..27375b5d3 100644
--- a/trunk/doc/CHANGELOG.md
+++ b/trunk/doc/CHANGELOG.md
@@ -7,6 +7,7 @@ The changelog for SRS.
## 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-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)
diff --git a/trunk/src/app/srs_app_rtc_api.cpp b/trunk/src/app/srs_app_rtc_api.cpp
index 23aa75c69..17d9f00d3 100644
--- a/trunk/src/app/srs_app_rtc_api.cpp
+++ b/trunk/src/app/srs_app_rtc_api.cpp
@@ -666,7 +666,7 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
string code_str;
if (true) {
srs_error_t err = srs_success;
-
+
err = serve_http_with(w, r);
if (err == srs_success) {
return err;
@@ -674,8 +674,8 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
code = srs_error_code(err);
code_str = srs_error_code_str(err);
- 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());
+ 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());
srs_freep(err);
}
diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp
index e68c373b7..74d7b6f31 100644
--- a/trunk/src/app/srs_app_rtc_conn.cpp
+++ b/trunk/src/app/srs_app_rtc_conn.cpp
@@ -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 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->set_codec_payload((SrsCodecPayload *)video_payload);
break;
@@ -3804,6 +3836,14 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig *
// @see https://bugs.chromium.org/p/webrtc/issues/detail?id=13166
track_descs = source->get_track_desc("video", "AV1X");
}
+ } else if (prefer_codec == SrsVideoCodecIdVP9) {
+ std::vector 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) {
std::vector payloads = remote_media_desc.find_media_with_encoding_name("H265");
if (payloads.empty()) {
diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp
index 635fb2262..6fc5b1968 100644
--- a/trunk/src/app/srs_app_rtc_source.cpp
+++ b/trunk/src/app/srs_app_rtc_source.cpp
@@ -3851,7 +3851,7 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio)
SrsAudioChannels channels = (SrsAudioChannels)audio_media->channel_;
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");
}
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_hevc_profile2str(profile).c_str(),
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());
}
}
diff --git a/trunk/src/app/srs_app_statistic.cpp b/trunk/src/app/srs_app_statistic.cpp
index 1bbaae113..b23f8430e 100644
--- a/trunk/src/app/srs_app_statistic.cpp
+++ b/trunk/src/app/srs_app_statistic.cpp
@@ -154,6 +154,12 @@ srs_error_t SrsStatisticStream::dumps(SrsJsonObject *obj)
} else if (vcodec_ == SrsVideoCodecIdHEVC) {
video->set("profile", SrsJsonAny::str(srs_hevc_profile2str(hevc_profile_).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 {
video->set("profile", SrsJsonAny::str("Other"));
video->set("level", SrsJsonAny::str("Other"));
diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp
index 4685600de..f2c2670e1 100644
--- a/trunk/src/core/srs_core_version7.hpp
+++ b/trunk/src/core/srs_core_version7.hpp
@@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
-#define VERSION_REVISION 122
+#define VERSION_REVISION 123
#endif
\ No newline at end of file
diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp
index 7d575efd9..cd84f81a3 100644
--- a/trunk/src/kernel/srs_kernel_codec.cpp
+++ b/trunk/src/kernel/srs_kernel_codec.cpp
@@ -82,6 +82,8 @@ string srs_video_codec_id2str(SrsVideoCodecId codec)
return "HEVC";
case SrsVideoCodecIdAV1:
return "AV1";
+ case SrsVideoCodecIdVP9:
+ return "VP9";
case SrsVideoCodecIdReserved:
case SrsVideoCodecIdReserved1:
case SrsVideoCodecIdReserved2:
@@ -105,6 +107,8 @@ SrsVideoCodecId srs_video_codec_str2id(const std::string &codec)
return SrsVideoCodecIdHEVC;
} else if (upper_codec == "AV1") {
return SrsVideoCodecIdAV1;
+ } else if (upper_codec == "VP9") {
+ return SrsVideoCodecIdVP9;
} else if (upper_codec == "VP6") {
return SrsVideoCodecIdOn2VP6;
} else if (upper_codec == "VP6A") {
diff --git a/trunk/src/kernel/srs_kernel_codec.hpp b/trunk/src/kernel/srs_kernel_codec.hpp
index ab838c786..ffdb97455 100644
--- a/trunk/src/kernel/srs_kernel_codec.hpp
+++ b/trunk/src/kernel/srs_kernel_codec.hpp
@@ -61,8 +61,10 @@ enum SrsVideoCodecId {
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
SrsVideoCodecIdHEVC = 12,
- // https://mp.weixin.qq.com/s/H3qI7zsON5sdf4oDJ9qlkg
+ // AV1 codec for WebRTC, https://github.com/ossrs/srs/pull/2324
SrsVideoCodecIdAV1 = 13,
+ // VP9 codec for WebRTC, https://github.com/ossrs/srs/pull/4565
+ SrsVideoCodecIdVP9 = 14,
};
std::string srs_video_codec_id2str(SrsVideoCodecId codec);
SrsVideoCodecId srs_video_codec_str2id(const std::string &codec);
diff --git a/trunk/src/kernel/srs_kernel_error.hpp b/trunk/src/kernel/srs_kernel_error.hpp
index 0fe936f32..329e15fb5 100644
--- a/trunk/src/kernel/srs_kernel_error.hpp
+++ b/trunk/src/kernel/srs_kernel_error.hpp
@@ -111,10 +111,9 @@
XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \
XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \
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")
-
/**************************************************/
/* RTMP protocol error. */
#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_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_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")
/**************************************************/
@@ -492,7 +491,7 @@ public:
// }
#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.
//
// Example:
@@ -501,8 +500,8 @@ public:
// }
#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
-// for converting internal errors to protocol-specific error codes (e.g.,
+// Transform an error by wrapping it and changing its error code. Useful
+// for converting internal errors to protocol-specific error codes (e.g.,
// HTTP, RTMP). The wrapped error chain is preserved.
//
// Example:
diff --git a/trunk/src/kernel/srs_kernel_ts.cpp b/trunk/src/kernel/srs_kernel_ts.cpp
index e84405d63..77020b4c1 100644
--- a/trunk/src/kernel/srs_kernel_ts.cpp
+++ b/trunk/src/kernel/srs_kernel_ts.cpp
@@ -443,6 +443,7 @@ srs_error_t SrsTsContext::encode(ISrsStreamWriter *writer, SrsTsMessage *msg, Sr
case SrsVideoCodecIdOn2VP6WithAlphaChannel:
case SrsVideoCodecIdScreenVideoVersion2:
case SrsVideoCodecIdAV1:
+ case SrsVideoCodecIdVP9:
vs = SrsTsStreamReserved;
break;
}
diff --git a/trunk/src/utest/srs_utest_ai18.cpp b/trunk/src/utest/srs_utest_ai18.cpp
index 5a6c5a679..99cbd812d 100644
--- a/trunk/src/utest/srs_utest_ai18.cpp
+++ b/trunk/src/utest/srs_utest_ai18.cpp
@@ -21,6 +21,7 @@ using namespace std;
#include
#include
#include
+#include
#include
// Mock ISrsSrtSocket implementation
@@ -193,10 +194,11 @@ VOID TEST(UdpListenerTest, ListenAndReceivePacket)
dest_addr.sin_port = htons(port);
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(),
(sockaddr *)&dest_addr, sizeof(dest_addr), SRS_UTIME_NO_TIMEOUT);
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
@@ -2186,39 +2188,8 @@ VOID TEST(ApiServerAsCandidatesTest, MajorUseScenario)
EXPECT_TRUE(candidate_ips.find("192.168.1.100") != candidate_ips.end());
}
-// Mock SrsProtocolUtility implementation
-MockProtocolUtility::MockProtocolUtility()
-{
-}
-
-MockProtocolUtility::~MockProtocolUtility()
-{
- clear_ips();
-}
-
-vector &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();
-}
+// Note: MockProtocolUtility has been merged into MockProtocolUtility
+// in srs_utest_manual_mock.hpp/cpp. All usages below now use MockProtocolUtility.
// Mock ISrsAppConfig for discover_candidates implementation
MockAppConfigForDiscoverCandidates::MockAppConfigForDiscoverCandidates()
diff --git a/trunk/src/utest/srs_utest_ai18.hpp b/trunk/src/utest/srs_utest_ai18.hpp
index d5261df02..5885b3ab0 100644
--- a/trunk/src/utest/srs_utest_ai18.hpp
+++ b/trunk/src/utest/srs_utest_ai18.hpp
@@ -196,19 +196,8 @@ public:
void clear_calls();
};
-// Mock SrsProtocolUtility for testing discover_candidates
-class MockProtocolUtility : public SrsProtocolUtility
-{
-public:
- std::vector mock_ips_;
-
-public:
- MockProtocolUtility();
- virtual ~MockProtocolUtility();
- virtual std::vector &local_ips();
- void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet);
- void clear_ips();
-};
+// Note: MockProtocolUtility has been merged into MockProtocolUtility
+// in srs_utest_manual_mock.hpp. Use MockProtocolUtility instead.
// Mock ISrsAppConfig for testing discover_candidates
class MockAppConfigForDiscoverCandidates : public MockAppConfig
diff --git a/trunk/src/utest/srs_utest_manual_mock.cpp b/trunk/src/utest/srs_utest_manual_mock.cpp
index 664794879..39cfe15f1 100644
--- a/trunk/src/utest/srs_utest_manual_mock.cpp
+++ b/trunk/src/utest/srs_utest_manual_mock.cpp
@@ -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
// Use member variables for SSRC and payload type values
@@ -104,7 +104,7 @@ std::string MockSdpFactory::create_chrome_player_offer()
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
// Use member variables for SSRC and payload type values
@@ -155,6 +155,113 @@ std::string MockSdpFactory::create_chrome_publisher_offer()
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()
{
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()
{
on_rtp_count_ = 0;
+ rtp_audio_count_ = 0;
+ rtp_video_count_ = 0;
}
MockRtcSource::~MockRtcSource()
@@ -315,6 +424,14 @@ MockRtcSource::~MockRtcSource()
srs_error_t MockRtcSource::on_rtp(SrsRtpPacket *pkt)
{
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);
}
@@ -2337,3 +2454,53 @@ void MockAudioTranscoder::aac_codec_header(uint8_t **data, int *len)
*data = copy;
*len = size;
}
+
+// Mock ISrsProtocolUtility implementation
+MockProtocolUtility::MockProtocolUtility(std::string ip)
+{
+ mock_ip_ = ip;
+}
+
+MockProtocolUtility::~MockProtocolUtility()
+{
+ clear_ips();
+}
+
+std::vector &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();
+}
diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp
index ea53599da..32a6087f0 100644
--- a/trunk/src/utest/srs_utest_manual_mock.hpp
+++ b/trunk/src/utest/srs_utest_manual_mock.hpp
@@ -78,9 +78,13 @@ public:
public:
// 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
- 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
@@ -162,6 +166,8 @@ class MockRtcSource : public SrsRtcSource
{
public:
int on_rtp_count_;
+ int rtp_audio_count_;
+ int rtp_video_count_;
public:
MockRtcSource();
@@ -1332,4 +1338,23 @@ public:
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 ips_;
+ std::string mock_ip_;
+
+public:
+ MockProtocolUtility(std::string ip = "");
+ virtual ~MockProtocolUtility();
+
+public:
+ virtual std::vector &local_ips();
+ void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet);
+ void clear_ips();
+};
+
#endif
diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp
index 8829f0335..f20afba53 100644
--- a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp
+++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp
@@ -34,73 +34,6 @@
#include
#include
-MockProtocolUtilityForRtcConn::MockProtocolUtilityForRtcConn(std::string ip)
-{
- mock_ip_ = ip;
-}
-
-MockProtocolUtilityForRtcConn::~MockProtocolUtilityForRtcConn()
-{
-}
-
-std::vector &MockProtocolUtilityForRtcConn::local_ips()
-{
- if (!ips_.empty()) {
- return ips_;
- }
-
- SrsIPAddress *addr = new SrsIPAddress();
- addr->ip_ = mock_ip_;
- addr->is_ipv4_ = true;
- addr->is_loopback_ = false; // Not loopback
- addr->is_internet_ = true; // Public IP
- addr->ifname_ = "eth0"; // Interface name
- ips_.push_back(addr);
-
- return ips_;
-}
-
-MockAppFactoryForRtcConn::MockAppFactoryForRtcConn()
-{
- mock_protocol_utility_ = NULL;
-}
-
-MockAppFactoryForRtcConn::~MockAppFactoryForRtcConn()
-{
-}
-
-ISrsProtocolUtility *MockAppFactoryForRtcConn::create_protocol_utility()
-{
- return mock_protocol_utility_;
-}
-
-ISrsRtcPublishStream *MockAppFactoryForRtcConn::create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid)
-{
- SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid);
- publisher->rtc_sources_ = rtc_sources_;
- return publisher;
-}
-
-MockRtcSourceForRtcConn::MockRtcSourceForRtcConn()
-{
- rtp_audio_count_ = 0;
- rtp_video_count_ = 0;
-}
-
-MockRtcSourceForRtcConn::~MockRtcSourceForRtcConn()
-{
-}
-
-srs_error_t MockRtcSourceForRtcConn::on_rtp(SrsRtpPacket *pkt)
-{
- if (pkt->frame_type_ == SrsFrameTypeAudio) {
- rtp_audio_count_++;
- } else if (pkt->frame_type_ == SrsFrameTypeVideo) {
- rtp_video_count_++;
- }
- return srs_success;
-}
-
// This test is used to verify the basic workflow of the RTC connection.
// It's finished with the help of AI, but each step is manually designed
// and verified. So this is not dominated by AI, but by humanbeing.
@@ -120,8 +53,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer)
mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
- mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100");
- MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn();
+ mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100");
+ MockRtcSource *mock_rtc_source = new MockRtcSource();
mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_
@@ -157,7 +90,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer)
ruc->srtp_ = true;
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_));
}
@@ -271,8 +204,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
mock_config->rtc_dtls_role_ = "passive";
mock_dtls_certificate->fingerprint_ = "test-fingerprint";
mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
- mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100");
- MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn();
+ mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100");
+ MockRtcSource *mock_rtc_source = new MockRtcSource();
mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source);
// Create a real ISrsRtcConnection using _srs_app_factory_
@@ -308,7 +241,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
ruc->srtp_ = true;
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_));
}
@@ -460,3 +393,433 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher)
// 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 mock_circuit_breaker(new MockCircuitBreaker());
+ SrsUniquePtr mock_conn_manager(new MockConnectionManager());
+ SrsUniquePtr mock_rtc_sources(new MockRtcSourceManager());
+ SrsUniquePtr mock_config(new MockAppConfig());
+ SrsUniquePtr mock_dtls_certificate(new MockDtlsCertificate());
+ SrsUniquePtr mock_sdp_factory(new MockSdpFactory());
+ SrsUniquePtr 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(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 conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
+ SrsRtcConnection *conn = dynamic_cast(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(conn->publisher_negotiator_);
+ pub_neg->config_ = mock_config.get();
+ SrsRtcPlayerNegotiator *play_neg = dynamic_cast(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 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 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(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 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 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 mock_circuit_breaker(new MockCircuitBreaker());
+ SrsUniquePtr mock_conn_manager(new MockConnectionManager());
+ SrsUniquePtr mock_rtc_sources(new MockRtcSourceManager());
+ SrsUniquePtr mock_config(new MockAppConfig());
+ SrsUniquePtr mock_dtls_certificate(new MockDtlsCertificate());
+ SrsUniquePtr mock_sdp_factory(new MockSdpFactory());
+ SrsUniquePtr 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(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 conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
+ SrsRtcConnection *conn = dynamic_cast(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(conn->publisher_negotiator_);
+ pub_neg->config_ = mock_config.get();
+ SrsRtcPlayerNegotiator *play_neg = dynamic_cast(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 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 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(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 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 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();
+}
diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp
index 6ee035a6e..14dfae20a 100644
--- a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp
+++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp
@@ -30,54 +30,34 @@
#include
#include
-#include
-
-#include
-#include
-
-class MockAppFactoryForRtcConn;
-
-class MockProtocolUtilityForRtcConn : public ISrsProtocolUtility
-{
-public:
- std::vector ips_;
- std::string mock_ip_;
-
-public:
- MockProtocolUtilityForRtcConn(std::string ip);
- virtual ~MockProtocolUtilityForRtcConn();
-
-public:
- virtual std::vector &local_ips();
-};
+#include
class MockAppFactoryForRtcConn : public SrsAppFactory
{
public:
ISrsRtcSourceManager *rtc_sources_;
- MockProtocolUtilityForRtcConn *mock_protocol_utility_;
+ MockProtocolUtility *mock_protocol_utility_;
public:
- MockAppFactoryForRtcConn();
- virtual ~MockAppFactoryForRtcConn();
+ MockAppFactoryForRtcConn()
+ {
+ mock_protocol_utility_ = NULL;
+ }
+ virtual ~MockAppFactoryForRtcConn()
+ {
+ }
public:
- virtual ISrsProtocolUtility *create_protocol_utility();
- virtual ISrsRtcPublishStream *create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid);
-};
-
-class MockRtcSourceForRtcConn : public SrsRtcSource
-{
-public:
- int rtp_audio_count_;
- int rtp_video_count_;
-
-public:
- MockRtcSourceForRtcConn();
- virtual ~MockRtcSourceForRtcConn();
-
-public:
- virtual srs_error_t on_rtp(SrsRtpPacket *pkt);
+ virtual ISrsProtocolUtility *create_protocol_utility()
+ {
+ return mock_protocol_utility_;
+ }
+ virtual ISrsRtcPublishStream *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;
+ }
};
#endif