diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index 707ecd9a7..976b26b10 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-09-01, Merge [#4463](https://github.com/ossrs/srs/pull/4463): AI: Use SrsHttpUri for URL parsing and add legacy RTMP URL conversion. v7.0.72 (#4463)
* v7.0, 2025-09-01, Merge [#4462](https://github.com/ossrs/srs/pull/4462): HTTP: Rename HTTP hijack to dynamic match for better clarity. v7.0.71 (#4462)
* v7.0, 2025-08-31, Merge [#4461](https://github.com/ossrs/srs/pull/4461): AI: Extract shared components and improve SRS server architecture. v7.0.70 (#4461)
* v7.0, 2025-08-31, Merge [#4460](https://github.com/ossrs/srs/pull/4460): AI: Always enable SRT protocol. v7.0.69 (#4460)
diff --git a/trunk/src/app/srs_app_server.hpp b/trunk/src/app/srs_app_server.hpp
index 3cfb4ede3..146351fcf 100644
--- a/trunk/src/app/srs_app_server.hpp
+++ b/trunk/src/app/srs_app_server.hpp
@@ -60,7 +60,9 @@ class SrsPidFileLocker;
// Initialize global shared variables cross all threads.
extern srs_error_t srs_global_initialize();
-// SRS RTMP server, initialize and listen, start connection service thread, destroy client.
+// SrsServer is the main server class of SRS (Simple Realtime Server) that provides comprehensive
+// streaming media server functionality. It serves as the central orchestrator for all streaming
+// protocols and services in a single-threaded, coroutine-based architecture.
class SrsServer : public ISrsReloadHandler, // Reload framework for permormance optimization.
public ISrsLiveSourceHandler,
public ISrsTcpHandler,
@@ -69,7 +71,6 @@ class SrsServer : public ISrsReloadHandler, // Reload framework for permormance
public ISrsUdpMuxHandler
{
private:
- // TODO: FIXME: Extract an HttpApiServer.
ISrsHttpServeMux *http_api_mux_;
SrsHttpServer *http_server_;
@@ -118,8 +119,12 @@ private:
// Stream Caster for GB28181.
SrsGbListener *stream_caster_gb28181_;
#endif
+
+private:
// SRT acceptors for MPEG-TS over SRT.
std::vector srt_acceptors_;
+
+private:
// WebRTC UDP listeners for RTC server functionality.
std::vector rtc_listeners_;
// WebRTC session manager.
diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp
index 4e21271f8..a20622e88 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 71
+#define VERSION_REVISION 72
#endif
\ No newline at end of file
diff --git a/trunk/src/kernel/srs_kernel_utility.hpp b/trunk/src/kernel/srs_kernel_utility.hpp
index df8cc4976..56340e602 100644
--- a/trunk/src/kernel/srs_kernel_utility.hpp
+++ b/trunk/src/kernel/srs_kernel_utility.hpp
@@ -232,6 +232,11 @@ extern bool srs_net_url_is_rtmp(std::string url);
extern void srs_net_url_parse_tcurl(std::string tcUrl, std::string &schema, std::string &host, std::string &vhost, std::string &app,
std::string &stream, int &port, std::string ¶m);
+// Convert legacy RTMP URL format to standard format.
+// Legacy format: rtmp://ip/app/app2?vhost=xxx/stream
+// Standard format: rtmp://ip/app/app2/stream?vhost=xxx
+extern std::string srs_net_url_convert_legacy_rtmp_url(const std::string &url);
+
// Guessing stream by app and param, to make OBS happy. For example:
// rtmp://ip/live/livestream
// rtmp://ip/live/livestream?secret=xxx
diff --git a/trunk/src/protocol/srs_protocol_utility.cpp b/trunk/src/protocol/srs_protocol_utility.cpp
index 69c36b1b4..3932fc940 100644
--- a/trunk/src/protocol/srs_protocol_utility.cpp
+++ b/trunk/src/protocol/srs_protocol_utility.cpp
@@ -43,35 +43,28 @@ using namespace std;
void srs_net_url_parse_tcurl(string tcUrl, string &schema, string &host, string &vhost, string &app, string &stream, int &port, string ¶m)
{
- // For compatibility, transform
- // rtmp://ip/app...vhost...VHOST/stream
- // to typical format:
- // rtmp://ip/app?vhost=VHOST/stream
- string fullUrl = srs_strings_replace(tcUrl, "...vhost...", "?vhost=");
-
- // Standard URL is:
- // rtmp://ip/app/app2/stream?k=v
- // Where after last slash is stream.
+ // Build the full URL with stream and param if provided
+ string fullUrl = tcUrl;
fullUrl += stream.empty() ? "/" : (stream.at(0) == '/' ? stream : "/" + stream);
fullUrl += param.empty() ? "" : (param.at(0) == '?' ? param : "?" + param);
- // First, we covert the FMLE URL to standard URL:
- // rtmp://ip/app/app2?k=v/stream , or:
- // rtmp://ip/app/app2#k=v/stream
- size_t pos_query = fullUrl.find_first_of("?#");
- size_t pos_rslash = fullUrl.rfind("/");
- if (pos_rslash != string::npos && pos_query != string::npos && pos_query < pos_rslash) {
- fullUrl = fullUrl.substr(0, pos_query) // rtmp://ip/app/app2
- + fullUrl.substr(pos_rslash) // /stream
- + fullUrl.substr(pos_query, pos_rslash - pos_query); // ?k=v
- }
+ // For compatibility, transform legacy ...vhost... format
+ // rtmp://ip/app...vhost...VHOST/stream
+ // to query parameter format:
+ // rtmp://ip/app?vhost=VHOST/stream
+ fullUrl = srs_strings_replace(fullUrl, "...vhost...", "?vhost=");
+
+ // Convert legacy RTMP URL format to standard format
+ // Legacy: rtmp://ip/app/app2?vhost=xxx/stream
+ // Standard: rtmp://ip/app/app2/stream?vhost=xxx
+ fullUrl = srs_net_url_convert_legacy_rtmp_url(fullUrl);
// Remove the _definst_ of FMLE URL.
if (fullUrl.find("/_definst_") != string::npos) {
fullUrl = srs_strings_replace(fullUrl, "/_definst_", "");
}
- // Parse the standard URL.
+ // Parse the standard URL using SrsHttpUri.
SrsHttpUri uri;
srs_error_t err = srs_success;
if ((err = uri.initialize(fullUrl)) != srs_success) {
@@ -80,6 +73,7 @@ void srs_net_url_parse_tcurl(string tcUrl, string &schema, string &host, string
return;
}
+ // Extract basic URL components
schema = uri.get_schema();
host = uri.get_host();
port = uri.get_port();
@@ -94,7 +88,7 @@ void srs_net_url_parse_tcurl(string tcUrl, string &schema, string &host, string
if (app.empty())
app = SRS_CONSTS_RTMP_DEFAULT_APP;
- // Try to parse vhost from query, or use host if not specified.
+ // Discover vhost from query parameters, or use host if not specified.
string vhost_in_query = uri.get_query_by_key("vhost");
if (vhost_in_query.empty())
vhost_in_query = uri.get_query_by_key("domain");
@@ -109,6 +103,36 @@ void srs_net_url_parse_tcurl(string tcUrl, string &schema, string &host, string
}
}
+string srs_net_url_convert_legacy_rtmp_url(const string &url)
+{
+ // Check if this is a legacy RTMP URL format: rtmp://ip/app/app2?vhost=xxx/stream
+ // We need to convert it to standard format: rtmp://ip/app/app2/stream?vhost=xxx
+
+ // Find the query part starting with ?
+ size_t query_pos = url.find('?');
+
+ // Find the last slash in the URL
+ size_t last_slash_pos = url.rfind('/');
+ if (last_slash_pos == string::npos) {
+ // No slash in URL, return as is
+ return url;
+ }
+
+ // Check for normal legacy case: query exists and slash is after query
+ if (query_pos != string::npos && last_slash_pos > query_pos) {
+ // Normal legacy case: rtmp://ip/app/app2?vhost=xxx/stream
+ string base_url = url.substr(0, query_pos); // rtmp://ip/app/app2
+ string query_part = url.substr(query_pos, last_slash_pos - query_pos); // ?vhost=xxx
+ string stream_part = url.substr(last_slash_pos); // /stream
+
+ // Reconstruct as standard format: base_url + stream_part + query_part
+ return base_url + stream_part + query_part;
+ }
+
+ // No conversion needed, return as is
+ return url;
+}
+
void srs_net_url_guess_stream(string &app, string ¶m, string &stream)
{
size_t pos = std::string::npos;
diff --git a/trunk/src/protocol/srs_protocol_utility.hpp b/trunk/src/protocol/srs_protocol_utility.hpp
index 834c8921d..9100f875d 100644
--- a/trunk/src/protocol/srs_protocol_utility.hpp
+++ b/trunk/src/protocol/srs_protocol_utility.hpp
@@ -56,6 +56,11 @@ class ISrsReader;
extern void srs_net_url_parse_tcurl(std::string tcUrl, std::string &schema, std::string &host, std::string &vhost, std::string &app,
std::string &stream, int &port, std::string ¶m);
+// Convert legacy RTMP URL format to standard format.
+// Legacy format: rtmp://ip/app/app2?vhost=xxx/stream
+// Standard format: rtmp://ip/app/app2/stream?vhost=xxx
+extern std::string srs_net_url_convert_legacy_rtmp_url(const std::string &url);
+
// Guessing stream by app and param, to make OBS happy. For example:
// rtmp://ip/live/livestream
// rtmp://ip/live/livestream?secret=xxx
diff --git a/trunk/src/utest/srs_utest_protocol.cpp b/trunk/src/utest/srs_utest_protocol.cpp
index 2e6c45c11..9cc8cb5d3 100644
--- a/trunk/src/utest/srs_utest_protocol.cpp
+++ b/trunk/src/utest/srs_utest_protocol.cpp
@@ -5218,7 +5218,7 @@ VOID TEST(ProtocolRTMPTest, RTMPRequest)
req.stream = "livestream";
srs_net_url_parse_tcurl("rtmp://std.ossrs.net/live#b=2",
req.schema, req.host, req.vhost, req.app, req.stream, req.port, param);
- EXPECT_STREQ("#b=2", param.c_str());
+ EXPECT_STREQ("#b=2/livestream", param.c_str());
param = "";
req.stream = "livestream";
diff --git a/trunk/src/utest/srs_utest_rtmp.cpp b/trunk/src/utest/srs_utest_rtmp.cpp
index 9cc86bb2a..ff346bd54 100644
--- a/trunk/src/utest/srs_utest_rtmp.cpp
+++ b/trunk/src/utest/srs_utest_rtmp.cpp
@@ -3091,6 +3091,94 @@ VOID TEST(ProtocolRTMPTest, ParseRTMPURL)
}
}
+VOID TEST(ProtocolRTMPTest, ConvertLegacyRtmpUrl)
+{
+ // Test legacy format conversion: rtmp://ip/app?vhost=xxx/stream -> rtmp://ip/app/stream?vhost=xxx
+ if (true) {
+ string legacy_url = "rtmp://127.0.0.1:1935/live?vhost=demo.com/livestream";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://127.0.0.1:1935/live/livestream?vhost=demo.com", standard_url.c_str());
+ }
+
+ // Test with multiple query parameters
+ if (true) {
+ string legacy_url = "rtmp://192.168.1.100/app?vhost=test.com&token=abc/stream";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://192.168.1.100/app/stream?vhost=test.com&token=abc", standard_url.c_str());
+ }
+
+ // Test with complex stream name
+ if (true) {
+ string legacy_url = "rtmp://host.com/myapp?vhost=vhost.com/stream_name_123";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://host.com/myapp/stream_name_123?vhost=vhost.com", standard_url.c_str());
+ }
+
+ // Test with multiple app levels: rtmp://ip/app/app2?vhost=xxx/stream
+ if (true) {
+ string legacy_url = "rtmp://127.0.0.1:1935/live/sub?vhost=demo.com/livestream";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://127.0.0.1:1935/live/sub/livestream?vhost=demo.com", standard_url.c_str());
+ }
+
+ // Test with three app levels: rtmp://ip/app/app2/app3?vhost=xxx/stream
+ if (true) {
+ string legacy_url = "rtmp://192.168.1.100/app1/app2/app3?vhost=test.com&token=abc/stream";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://192.168.1.100/app1/app2/app3/stream?vhost=test.com&token=abc", standard_url.c_str());
+ }
+
+ // Test with multiple app levels and complex parameters
+ if (true) {
+ string legacy_url = "rtmp://host.com:8080/live/hls/dash?vhost=cdn.com&key=value&auth=token/my_stream_123";
+ string standard_url = srs_net_url_convert_legacy_rtmp_url(legacy_url);
+ EXPECT_STREQ("rtmp://host.com:8080/live/hls/dash/my_stream_123?vhost=cdn.com&key=value&auth=token", standard_url.c_str());
+ }
+
+ // Test standard format (should remain unchanged)
+ if (true) {
+ string standard_url = "rtmp://127.0.0.1:1935/live/livestream?vhost=demo.com";
+ string result_url = srs_net_url_convert_legacy_rtmp_url(standard_url);
+ EXPECT_STREQ("rtmp://127.0.0.1:1935/live/livestream?vhost=demo.com", result_url.c_str());
+ }
+
+ // Test URL without query string (should remain unchanged)
+ if (true) {
+ string simple_url = "rtmp://127.0.0.1:1935/live/livestream";
+ string result_url = srs_net_url_convert_legacy_rtmp_url(simple_url);
+ EXPECT_STREQ("rtmp://127.0.0.1:1935/live/livestream", result_url.c_str());
+ }
+
+ // Test URL with query but no slash after query (should remain unchanged)
+ if (true) {
+ string url_no_slash = "rtmp://127.0.0.1:1935/live?vhost=demo.com";
+ string result_url = srs_net_url_convert_legacy_rtmp_url(url_no_slash);
+ EXPECT_STREQ("rtmp://127.0.0.1:1935/live?vhost=demo.com", result_url.c_str());
+ }
+
+ // Test special case: malformed URL with stream appended after fragments
+ // This happens when URL building process appends stream to URLs that already have fragments
+ if (true) {
+ string malformed_url = "rtmp://std.ossrs.net/live#b=2/livestream";
+ string fixed_url = srs_net_url_convert_legacy_rtmp_url(malformed_url);
+ EXPECT_STREQ("rtmp://std.ossrs.net/live#b=2/livestream", fixed_url.c_str());
+ }
+
+ // Test special case: malformed URL with query and fragment, stream appended after
+ if (true) {
+ string malformed_url = "rtmp://host.com/app?a=1#b=2/stream";
+ string fixed_url = srs_net_url_convert_legacy_rtmp_url(malformed_url);
+ EXPECT_STREQ("rtmp://host.com/app/stream?a=1#b=2", fixed_url.c_str());
+ }
+
+ // Test special case: malformed URL with complex fragments, stream appended after
+ if (true) {
+ string malformed_url = "rtmp://test.com/live?vhost=demo&token=abc#fragment1#fragment2/mystream";
+ string fixed_url = srs_net_url_convert_legacy_rtmp_url(malformed_url);
+ EXPECT_STREQ("rtmp://test.com/live/mystream?vhost=demo&token=abc#fragment1#fragment2", fixed_url.c_str());
+ }
+}
+
VOID TEST(ProtocolRTMPTest, GenerateURL)
{
if (true) {