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:
parent
1a96abc880
commit
7fcd406a63
31
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
31
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ The changelog for SRS.
|
|||
<a name="v7-changes"></a>
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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->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<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) {
|
||||
std::vector<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("H265");
|
||||
if (payloads.empty()) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
#define VERSION_MAJOR 7
|
||||
#define VERSION_MINOR 0
|
||||
#define VERSION_REVISION 122
|
||||
#define VERSION_REVISION 123
|
||||
|
||||
#endif
|
||||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using namespace std;
|
|||
#include <srs_kernel_utility.hpp>
|
||||
#include <srs_protocol_utility.hpp>
|
||||
#include <srs_utest_ai23.hpp>
|
||||
#include <srs_utest_manual_mock.hpp>
|
||||
#include <sstream>
|
||||
|
||||
// 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<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();
|
||||
}
|
||||
// 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()
|
||||
|
|
|
|||
|
|
@ -196,19 +196,8 @@ public:
|
|||
void clear_calls();
|
||||
};
|
||||
|
||||
// Mock SrsProtocolUtility for testing discover_candidates
|
||||
class MockProtocolUtility : public SrsProtocolUtility
|
||||
{
|
||||
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();
|
||||
};
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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<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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
|
|
|
|||
|
|
@ -34,73 +34,6 @@
|
|||
#include <srs_utest_manual_mock.hpp>
|
||||
#include <srs_utest_manual_service.hpp>
|
||||
|
||||
MockProtocolUtilityForRtcConn::MockProtocolUtilityForRtcConn(std::string ip)
|
||||
{
|
||||
mock_ip_ = ip;
|
||||
}
|
||||
|
||||
MockProtocolUtilityForRtcConn::~MockProtocolUtilityForRtcConn()
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<SrsIPAddress *> &MockProtocolUtilityForRtcConn::local_ips()
|
||||
{
|
||||
if (!ips_.empty()) {
|
||||
return ips_;
|
||||
}
|
||||
|
||||
SrsIPAddress *addr = new SrsIPAddress();
|
||||
addr->ip_ = mock_ip_;
|
||||
addr->is_ipv4_ = true;
|
||||
addr->is_loopback_ = false; // Not loopback
|
||||
addr->is_internet_ = true; // Public IP
|
||||
addr->ifname_ = "eth0"; // Interface name
|
||||
ips_.push_back(addr);
|
||||
|
||||
return ips_;
|
||||
}
|
||||
|
||||
MockAppFactoryForRtcConn::MockAppFactoryForRtcConn()
|
||||
{
|
||||
mock_protocol_utility_ = NULL;
|
||||
}
|
||||
|
||||
MockAppFactoryForRtcConn::~MockAppFactoryForRtcConn()
|
||||
{
|
||||
}
|
||||
|
||||
ISrsProtocolUtility *MockAppFactoryForRtcConn::create_protocol_utility()
|
||||
{
|
||||
return mock_protocol_utility_;
|
||||
}
|
||||
|
||||
ISrsRtcPublishStream *MockAppFactoryForRtcConn::create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid)
|
||||
{
|
||||
SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid);
|
||||
publisher->rtc_sources_ = rtc_sources_;
|
||||
return publisher;
|
||||
}
|
||||
|
||||
MockRtcSourceForRtcConn::MockRtcSourceForRtcConn()
|
||||
{
|
||||
rtp_audio_count_ = 0;
|
||||
rtp_video_count_ = 0;
|
||||
}
|
||||
|
||||
MockRtcSourceForRtcConn::~MockRtcSourceForRtcConn()
|
||||
{
|
||||
}
|
||||
|
||||
srs_error_t MockRtcSourceForRtcConn::on_rtp(SrsRtpPacket *pkt)
|
||||
{
|
||||
if (pkt->frame_type_ == SrsFrameTypeAudio) {
|
||||
rtp_audio_count_++;
|
||||
} else if (pkt->frame_type_ == SrsFrameTypeVideo) {
|
||||
rtp_video_count_++;
|
||||
}
|
||||
return srs_success;
|
||||
}
|
||||
|
||||
// This test is used to verify the basic workflow of the RTC connection.
|
||||
// It's finished with the help of AI, but each step is manually designed
|
||||
// and verified. So this is not dominated by AI, but by humanbeing.
|
||||
|
|
@ -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<SrsRtcSource>(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<SrsRtcSource>(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<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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,54 +30,34 @@
|
|||
#include <srs_utest.hpp>
|
||||
|
||||
#include <srs_app_factory.hpp>
|
||||
#include <srs_protocol_utility.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();
|
||||
};
|
||||
#include <srs_utest_manual_mock.hpp>
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user