diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf
index df40356cb..2fcfca22e 100644
--- a/trunk/conf/full.conf
+++ b/trunk/conf/full.conf
@@ -1540,8 +1540,11 @@ vhost hls.srs.com {
hls_key_file_path ./objs/nginx/html;
# the key root URL, use this can support https.
# @remark It's optional.
+ # @remark Supports query string for authentication tokens (e.g., JWT).
+ # Example: http://localhost:8080/?token=abc123&sig=xyz789
+ # Result in m3u8: http://localhost:8080/live/livestream-0.key?token=abc123&sig=xyz789
# Overwrite by env SRS_VHOST_HLS_HLS_KEY_URL for all vhosts.
- hls_key_url https://localhost:8080;
+ hls_key_url http://localhost:8080/;
# Special control controls.
###########################################
diff --git a/trunk/conf/hls-encrypted-query.conf b/trunk/conf/hls-encrypted-query.conf
new file mode 100644
index 000000000..7263f7365
--- /dev/null
+++ b/trunk/conf/hls-encrypted-query.conf
@@ -0,0 +1,35 @@
+# Test config for HLS encryption with query string in hls_key_url
+# This tests the fix for issue #4426
+
+listen 1935;
+max_connections 1000;
+daemon off;
+srs_log_tank console;
+
+http_server {
+ enabled on;
+ listen 8080;
+ dir ./objs/nginx/html;
+}
+
+vhost __defaultVhost__ {
+ hls {
+ enabled on;
+ hls_fragment 10;
+ hls_window 60;
+ hls_path ./objs/nginx/html;
+ hls_m3u8_file [app]/[stream].m3u8;
+ hls_ts_file [app]/[stream]-[seq].ts;
+
+ # Enable AES-128 encryption
+ hls_keys on;
+ hls_fragments_per_key 5;
+ hls_key_file [app]/[stream]-[seq].key;
+ hls_key_file_path ./objs/nginx/html;
+
+ # Test with query string - this should now work correctly
+ # Expected result in m3u8: http://localhost:8080/live/livestream-0.key?token=abc123&sig=xyz789
+ hls_key_url http://localhost:8080/?token=abc123&sig=xyz789;
+ }
+}
+
diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index e78cef07d..afd74ff51 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-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426)
* v7.0, 2025-11-07, AI: RTC: Support keep_original_ssrc to preserve SSRC and timestamps. v7.0.119 (#3850)
* v7.0, 2025-11-05, AI: WebRTC: Report video/audio codec info and frame stats in HTTP API. v7.0.118 (#4554)
* v7.0, 2025-11-04, AI: SRT: Report video/audio codec info and frame stats in HTTP API. v7.0.117 (#4554)
diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp
index dcec05d49..6c5572697 100644
--- a/trunk/src/app/srs_app_hls.cpp
+++ b/trunk/src/app/srs_app_hls.cpp
@@ -33,6 +33,7 @@ using namespace std;
#include
#include
#include
+#include
#include
#include
@@ -45,6 +46,33 @@ using namespace std;
// reset the piece id when deviation overflow this.
#define SRS_JUMP_WHEN_PIECE_DEVIATION 20
+// Build the full key URL by appending key_file to hls_key_url with proper query string handling.
+// If hls_key_url contains query string like "http://localhost:8080/?token=abc",
+// the result will be "http://localhost:8080/live/livestream-0.key?token=abc"
+// @param hls_key_url The base URL which may contain query string
+// @param key_file The key file path like "live/livestream-0.key"
+// @return The full key URL with query string properly appended
+string srs_hls_build_key_url(const string &hls_key_url, const string &key_file)
+{
+ if (hls_key_url.empty()) {
+ return key_file;
+ }
+
+ // Find the query string separator
+ size_t pos = hls_key_url.find("?");
+ if (pos != string::npos) {
+ // URL contains query string, split and rebuild
+ // Example: "http://localhost:8080/?token=abc" + "live/livestream-0.key"
+ // Result: "http://localhost:8080/live/livestream-0.key?token=abc"
+ string base_url = hls_key_url.substr(0, pos);
+ string query_string = hls_key_url.substr(pos); // Include the '?'
+ return base_url + key_file + query_string;
+ }
+
+ // No query string, simple concatenation
+ return hls_key_url + key_file;
+}
+
SrsHlsSegment::SrsHlsSegment(SrsTsContext *c, SrsAudioCodecId ac, SrsVideoCodecId vc, ISrsFileWriter *w)
{
sequence_no_ = 0;
@@ -1107,11 +1135,7 @@ srs_error_t SrsHlsFmp4Muxer::do_refresh_m3u8_segment(SrsHlsM4sSegment *segment,
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
- string key_path = key_file;
- // if key_url is not set,only use the file name
- if (!hls_key_url_.empty()) {
- key_path = hls_key_url_ + key_file;
- }
+ string key_path = srs_hls_build_key_url(hls_key_url_, key_file);
ss << "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
@@ -2040,11 +2064,7 @@ srs_error_t SrsHlsMuxer::do_refresh_m3u8_segment(SrsHlsSegment *segment, std::st
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
- string key_path = key_file;
- // if key_url is not set,only use the file name
- if (!hls_key_url_.empty()) {
- key_path = hls_key_url_ + key_file;
- }
+ string key_path = srs_hls_build_key_url(hls_key_url_, key_file);
ss << "#EXT-X-KEY:METHOD=AES-128,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp
index 9ca5f80fb..9ccc2ecd5 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 119
+#define VERSION_REVISION 120
#endif
\ No newline at end of file
diff --git a/trunk/src/utest/srs_utest_ai21.hpp b/trunk/src/utest/srs_utest_ai21.hpp
index 3ecdd04ad..1ba6eace0 100644
--- a/trunk/src/utest/srs_utest_ai21.hpp
+++ b/trunk/src/utest/srs_utest_ai21.hpp
@@ -111,6 +111,7 @@ public:
void reset();
};
+#ifdef SRS_RTSP
// Forward declaration
class SrsRtspConsumer;
@@ -159,5 +160,6 @@ public:
void set_send_error(srs_error_t err);
void reset();
};
+#endif
#endif
diff --git a/trunk/src/utest/srs_utest_ai22.cpp b/trunk/src/utest/srs_utest_ai22.cpp
index 1f5d583f1..cf7f8f07a 100644
--- a/trunk/src/utest/srs_utest_ai22.cpp
+++ b/trunk/src/utest/srs_utest_ai22.cpp
@@ -1549,6 +1549,7 @@ void MockStatisticForRtspPlayStream::reset()
srs_freep(on_client_error_);
}
+#ifdef SRS_RTSP
// MockRtspSourceManager implementation
MockRtspSourceManager::MockRtspSourceManager()
{
@@ -2750,6 +2751,7 @@ VOID TEST(RtspTcpNetworkTest, WriteRtpPacket)
// Verify the payload data (starts at offset 4)
EXPECT_EQ(0, memcmp(rtp_packet, output + 4, kRtpPacketSize));
}
+#endif
// MockDvrPlan implementation
MockDvrPlan::MockDvrPlan()
diff --git a/trunk/src/utest/srs_utest_ai22.hpp b/trunk/src/utest/srs_utest_ai22.hpp
index a1b580df0..ef9fdc948 100644
--- a/trunk/src/utest/srs_utest_ai22.hpp
+++ b/trunk/src/utest/srs_utest_ai22.hpp
@@ -354,6 +354,7 @@ public:
void reset();
};
+#ifdef SRS_RTSP
// Mock ISrsRtspSourceManager for testing SrsRtspPlayStream
class MockRtspSourceManager : public ISrsRtspSourceManager
{
@@ -456,6 +457,7 @@ public:
virtual void set_all_tracks_status(bool status);
void reset();
};
+#endif
// Mock ISrsDvrPlan for testing SrsDvrSegmenter
class MockDvrPlan : public ISrsDvrPlan
diff --git a/trunk/src/utest/srs_utest_ai23.cpp b/trunk/src/utest/srs_utest_ai23.cpp
index 879e73d70..dd616ec3a 100644
--- a/trunk/src/utest/srs_utest_ai23.cpp
+++ b/trunk/src/utest/srs_utest_ai23.cpp
@@ -29,6 +29,7 @@ using namespace std;
#include
#include
+#ifdef SRS_GB28181
// Mock ISrsGbMuxer implementation
MockGbMuxer::MockGbMuxer()
{
@@ -2294,6 +2295,7 @@ VOID TEST(GB28181Test, GoApiGbPublishSuccess)
srs_freep(conf);
}
+#endif
// Mock ISrsRtcNetwork implementation
MockRtcNetworkForNetworks::MockRtcNetworkForNetworks()
diff --git a/trunk/src/utest/srs_utest_ai23.hpp b/trunk/src/utest/srs_utest_ai23.hpp
index ab8cf2ca3..32fa9aeab 100644
--- a/trunk/src/utest/srs_utest_ai23.hpp
+++ b/trunk/src/utest/srs_utest_ai23.hpp
@@ -30,6 +30,7 @@
#include
#endif
+#ifdef SRS_GB28181
// Mock ISrsGbMuxer for testing SrsGbSession
class MockGbMuxer : public ISrsGbMuxer
{
@@ -541,6 +542,7 @@ public:
#endif
void reset();
};
+#endif
// Mock ISrsRtcNetwork for testing SrsRtcNetworks
class MockRtcNetworkForNetworks : public ISrsRtcNetwork
diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp
index dbdd005bb..bd8b34107 100644
--- a/trunk/src/utest/srs_utest_manual_mock.hpp
+++ b/trunk/src/utest/srs_utest_manual_mock.hpp
@@ -517,6 +517,7 @@ public:
virtual int get_rtc_drop_for_pt(std::string vhost) { return rtc_drop_for_pt_; }
virtual bool get_rtc_twcc_enabled(std::string vhost) { return rtc_twcc_enabled_; }
virtual bool get_rtc_init_rate_from_sdp(std::string vhost) { return rtc_init_rate_from_sdp_; }
+ virtual bool get_rtc_keep_original_ssrc(std::string vhost) { return false; }
virtual bool get_srt_enabled() { return srt_enabled_; }
virtual bool get_srt_enabled(std::string vhost) { return srt_enabled_; }
virtual std::string get_srt_default_streamid() { return "#!::r=live/livestream,m=request"; }