diff --git a/src/Http/HlsPlayer.cpp b/src/Http/HlsPlayer.cpp index 38e4e263..4c5f8eaf 100644 --- a/src/Http/HlsPlayer.cpp +++ b/src/Http/HlsPlayer.cpp @@ -22,6 +22,8 @@ HlsPlayer::HlsPlayer(const EventPoller::Ptr &poller) { void HlsPlayer::play(const string &url) { _play_result = false; _play_url = url; + _last_sequence = -1; + _playlist_reload_changed = true; setProxyUrl((*this)[Client::kProxyUrl]); setAllowResendRequest(true); fetchIndexFile(); @@ -202,10 +204,12 @@ void HlsPlayer::fetchSegment() { bool HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map &ts_map) { if (!is_m3u8_inner) { + auto playlist_changed = _last_sequence != sequence; + _playlist_reload_changed = playlist_changed; // 这是ts播放列表 [AUTO-TRANSLATED:7ce3d81b] // This is the ts playlist // This is the ts playlist - if (_last_sequence == sequence) { + if (!playlist_changed) { // 如果是重复的ts列表,那么忽略 [AUTO-TRANSLATED:d15a47f3] // If it is a duplicate ts list, then ignore it // 但是需要注意, 如果当前ts列表为空了, 那么表明直播结束了或者m3u8文件有问题,需要重新拉流 [AUTO-TRANSLATED:438a8df0] @@ -312,27 +316,23 @@ float HlsPlayer::delaySecond() { if (HlsParser::isM3u8() && HlsParser::getTargetDur() > 0) { float targetOffset; if (HlsParser::isLive()) { - // see RFC 8216, Section 4.4.3.8. - // 根据rfc刷新index列表的周期应该是分段时间x3, 因为根据规范播放器只处理最后3个Segment [AUTO-TRANSLATED:07168708] - // According to the rfc, the refresh cycle of the index list should be 3 times the segment time, because according to the specification, the player only processes the last 3 Segments - // refresh the index list according to rfc cycle should be the segment time x3, - // because according to the specification, the player only handles the last 3 segments - targetOffset = (float)(3 * HlsParser::getTargetDur()); + // RFC 8216 Section 6.3.4: + // after a changed playlist reload, wait at least one target + // duration; after an unchanged reload, wait half a target duration. + return _playlist_reload_changed ? (float) HlsParser::getTargetDur() + : (float) HlsParser::getTargetDur() / 2.0f; } else { // 点播则一般m3u8文件不会在改变了, 没必要频繁的刷新, 所以按照总时间来进行刷新 [AUTO-TRANSLATED:2ac0a29e] // On-demand generally does not change the m3u8 file, there is no need to refresh frequently, so refresh according to the total time - // On-demand, the m3u8 file will generally not change, so there is no need to refresh frequently, targetOffset = HlsParser::getTotalDuration(); } // 取最小值, 避免因为分段时长不规则而导致的问题 [AUTO-TRANSLATED:073dff48] // Take the minimum value to avoid problems caused by irregular segment durations - // Take the minimum value to avoid problems caused by irregular segment duration if (targetOffset > HlsParser::getTotalDuration()) { targetOffset = HlsParser::getTotalDuration(); } // 根据规范为一半的时间 [AUTO-TRANSLATED:07652637] // According to the specification, it is half the time - // According to the specification, it is half the time if (targetOffset / 2 > 1.0f) { return targetOffset / 2; } diff --git a/src/Http/HlsPlayer.h b/src/Http/HlsPlayer.h index ff797354..3f3d965a 100644 --- a/src/Http/HlsPlayer.h +++ b/src/Http/HlsPlayer.h @@ -130,6 +130,10 @@ private: int _timeout_multiple = MIN_TIMEOUT_MULTIPLE; int _try_fetch_index_times = 0; int _ts_download_failed_count = 0; + // RFC 8216 reload interval depends on whether the last media sequence + // changed. We intentionally keep this lightweight and only track the + // sequence number for live playlist refresh timing. + bool _playlist_reload_changed = true; protected: size_t _recvtotalbytes = 0; diff --git a/src/Http/HttpClient.cpp b/src/Http/HttpClient.cpp index ea39c826..4ee3f375 100644 --- a/src/Http/HttpClient.cpp +++ b/src/Http/HttpClient.cpp @@ -18,6 +18,15 @@ using namespace toolkit; namespace mediakit { +static bool connectionContainsClose(const string &connection) { + for (auto token : split(connection, ",")) { + if (strToLower(trim(std::move(token))) == "close") { + return true; + } + } + return false; +} + void HttpClient::sendRequest(const string &url) { clearResponse(); _url = url; @@ -56,26 +65,24 @@ void HttpClient::sendRequest(const string &url) { } auto host_header = host; splitUrl(host, host, port); + auto keep_alive = _request_keep_alive; + auto persistent = _http_persistent && keep_alive; + bool protocol_changed = (_is_https != is_https); + bool host_changed = (_last_host != host + ":" + to_string(port)) || protocol_changed; + _last_host = host + ":" + to_string(port); + _is_https = is_https; + _header.emplace("Host", host_header); _header.emplace("User-Agent", kServerName); _header.emplace("Accept", "*/*"); _header.emplace("Accept-Language", "zh-CN,zh;q=0.8"); - if (_http_persistent) { - _header.emplace("Connection", "keep-alive"); - } else { - _header.emplace("Connection", "close"); - } - _http_persistent = true; + _header.emplace("Connection", keep_alive ? "keep-alive" : "close"); if (_body && _body->remainSize()) { _header.emplace("Content-Length", to_string(_body->remainSize())); GET_CONFIG(string, charSet, Http::kCharSet); _header.emplace("Content-Type", "application/x-www-form-urlencoded; charset=" + charSet); } - bool host_changed = (_last_host != host + ":" + to_string(port)) || (_is_https != is_https); - _last_host = host + ":" + to_string(port); - _is_https = is_https; - auto cookies = HttpCookieStorage::Instance().get(_last_host, _path); _StrPrinter printer; for (auto &cookie : cookies) { @@ -85,13 +92,24 @@ void HttpClient::sendRequest(const string &url) { printer.pop_back(); _header.emplace("Cookie", printer); } - if (!alive() || host_changed || !_http_persistent) { - if (isUsedProxy()) { + if (isUsedProxy()) { + // All proxy traffic uses CONNECT, so reuse is limited to the same tunnel target. + bool proxy_reuse = alive() && persistent && !host_changed && _proxy_connected; + + if (!proxy_reuse) { + _http_persistent = keep_alive; _proxy_connected = false; - startConnect(_proxy_host, _proxy_port, _wait_header_ms / 1000.0f); + startConnectWithProxy(host, _proxy_host, _proxy_port, _wait_header_ms / 1000.0f); } else { - startConnect(host, port, _wait_header_ms / 1000.0f); + SockException ex; + onConnect_l(ex); } + return; + } + + if (!alive() || host_changed || !persistent) { + _http_persistent = keep_alive; + startConnect(host, port, _wait_header_ms / 1000.0f); } else { SockException ex; onConnect_l(ex); @@ -103,8 +121,9 @@ void HttpClient::clear() { _user_set_header.clear(); _body.reset(); _method.clear(); - // 重置代理连接状态 - _proxy_connected = false; + _request_keep_alive = true; + // Keep transport-level state so a live direct/proxy connection can still + // be reused after the caller resets only the per-request state. clearResponse(); } @@ -202,9 +221,8 @@ void HttpClient::onRecv(const Buffer::Ptr &pBuf) { void HttpClient::onError(const SockException &ex) { if (ex.getErrCode() == Err_reset && _allow_resend_request && _http_persistent && _recved_body_size == 0 && !_header_recved) { - // 连接被重置,可能是服务器主动断开了连接, 或者服务器内核参数或防火墙的持久连接空闲时间超时或不一致. [AUTO-TRANSLATED:8a78f452] - // The connection was reset, possibly because the server actively closed the connection, or the server kernel parameters or firewall's persistent connection idle timeout or inconsistency. - // 如果是持久化连接,那么我们可以通过重连来解决这个问题 [AUTO-TRANSLATED:6c113e17] + // 连接被重置,可能是服务器主动断开了连接, 或者服务器内核参数或防火墙的持久连接空闲时间超时或不一致. + // 如果是持久化连接,那么我们可以通过重连来解决这个问题 // If it is a persistent connection, we can solve this problem by reconnecting // The connection was reset, possibly because the server actively disconnected the connection, // or the persistent connection idle time of the server kernel parameters or firewall timed out or inconsistent. @@ -219,12 +237,17 @@ void HttpClient::onError(const SockException &ex) { ssize_t HttpClient::onRecvHeader(const char *data, size_t len) { _parser.parse(data, len); - if (_parser.status() == "302" || _parser.status() == "301" || _parser.status() == "303") { + auto connection_close = connectionContainsClose(_parser["Connection"]); + if (connection_close) { + _http_persistent = false; + } + if (_parser.status() == "302" || _parser.status() == "301" || _parser.status() == "303" || _parser.status() == "307") { auto new_url = Parser::mergeUrl(_url, _parser["Location"]); if (new_url.empty()) { throw invalid_argument("未找到Location字段(跳转url)"); } - if (onRedirectUrl(new_url, _parser.status() == "302")) { + bool temporary_redirect = _parser.status() == "302" || _parser.status() == "307"; + if (onRedirectUrl(new_url, temporary_redirect)) { HttpClient::sendRequest(new_url); return 0; } @@ -467,6 +490,13 @@ void HttpClient::setCompleteTimeout(size_t timeout_ms) { _wait_complete_ms = timeout_ms; } +void HttpClient::setRequestKeepAlive(bool enable) { + _request_keep_alive = enable; + if (!enable) { + _http_persistent = false; + } +} + bool HttpClient::isUsedProxy() const { return _used_proxy; } @@ -476,12 +506,34 @@ bool HttpClient::isProxyConnected() const { } void HttpClient::setProxyUrl(string proxy_url) { + auto old_used_proxy = _used_proxy; + auto old_proxy_host = _proxy_host; + auto old_proxy_port = _proxy_port; + auto old_proxy_auth = _proxy_auth; + _proxy_url = std::move(proxy_url); if (!_proxy_url.empty()) { + _proxy_host.clear(); + _proxy_port = 0; + _proxy_auth.clear(); parseProxyUrl(_proxy_url, _proxy_host, _proxy_port, _proxy_auth); _used_proxy = true; } else { _used_proxy = false; + _proxy_host.clear(); + _proxy_port = 0; + _proxy_auth.clear(); + _proxy_connected = false; + } + + auto proxy_config_changed = old_used_proxy != _used_proxy + || old_proxy_host != _proxy_host + || old_proxy_port != _proxy_port + || old_proxy_auth != _proxy_auth; + if (proxy_config_changed) { + // A proxy mode or endpoint change must not reuse the previous transport. + _http_persistent = false; + _proxy_connected = false; } } @@ -493,6 +545,10 @@ bool HttpClient::checkProxyConnected(const char *data, size_t len) { } _proxy_connected = false; + // CONNECT failed, which usually means the proxy rejected the tunnel request, + // does not support CONNECT for this target, or the proxy authentication is invalid. + WarnL << "proxy CONNECT failed, status line: " + << response.substr(0, response.find("\r\n")); return false; } diff --git a/src/Http/HttpClient.h b/src/Http/HttpClient.h index c7ca2433..caaa7a03 100644 --- a/src/Http/HttpClient.h +++ b/src/Http/HttpClient.h @@ -60,8 +60,8 @@ public: virtual void sendRequest(const std::string &url); /** - * 重置对象 - * Reset object + * 重置当前请求相关状态,并尽量保留可复用的传输状态 + * Reset per-request state while preserving reusable transport state when possible * [AUTO-TRANSLATED:d23b5bbb] */ @@ -168,10 +168,10 @@ public: void setHeaderTimeout(size_t timeout_ms); /** - * 设置接收body数据超时时间, 默认5秒 + * 设置接收body数据超时时间, 默认10秒 * 此参数可以用于处理超大body回复的超时问题 * 此参数可以等于0 - * Set the timeout for receiving body data, default 5 seconds + * Set the timeout for receiving body data, default 10 seconds * This parameter can be used to handle timeout issues for large body responses * This parameter can be equal to 0 @@ -189,6 +189,14 @@ public: */ void setCompleteTimeout(size_t timeout_ms); + /** + * 设置请求头中的 keep-alive 语义,默认启用 + * Set whether requests should advertise keep-alive semantics, enabled by default + + * [AUTO-TRANSLATED:6f62f63c] + */ + void setRequestKeepAlive(bool enable); + /** * 设置http代理url * Set http proxy url @@ -276,7 +284,6 @@ private: void onResponseCompleted_l(const toolkit::SockException &ex); void onConnect_l(const toolkit::SockException &ex); void checkCookie(HttpHeader &headers); - private: //for http response bool _complete = false; @@ -289,7 +296,7 @@ private: std::shared_ptr _chunked_splitter; //for request args - bool _is_https; + bool _is_https = false; std::string _url; HttpHeader _user_set_header; HttpBody::Ptr _body; @@ -308,9 +315,10 @@ private: toolkit::Ticker _wait_body; toolkit::Ticker _wait_complete; + bool _request_keep_alive = true; bool _used_proxy = false; bool _proxy_connected = false; - uint16_t _proxy_port; + uint16_t _proxy_port = 0; std::string _proxy_url; std::string _proxy_host; std::string _proxy_auth;