From 47efffab380aad40d9532b7edcc0b3fb714809d5 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Fri, 12 Jun 2026 12:31:03 +0800 Subject: [PATCH 1/2] feat(seek): support independent seek offsets --- docs/en/guide/time-processing.md | 7 +- docs/en/guide/url-formats.md | 6 +- docs/guide/time-processing.md | 7 +- docs/guide/url-formats.md | 6 +- e2e/test_rtsp_seek_mode.py | 50 ++++++++++++ e2e/test_url_template.py | 99 +++++++++++++++++++++++ src/service.c | 134 +++++++++++++++++++++++-------- src/service.h | 23 +++--- src/stream.c | 16 ++-- src/url_template.h | 3 +- 10 files changed, 288 insertions(+), 63 deletions(-) diff --git a/docs/en/guide/time-processing.md b/docs/en/guide/time-processing.md index 4923aa7..b7fb093 100644 --- a/docs/en/guide/time-processing.md +++ b/docs/en/guide/time-processing.md @@ -57,12 +57,15 @@ If your upstream uses a different parameter name (e.g. `seek`, `timeshift`), spe ### r2h-seek-offset (optional) -Adds or subtracts a number of seconds to the recognized time-shift time, positive or negative. Commonly used to compensate for clock drift on the upstream server, or to shift the start time earlier/later as a whole. +Adds or subtracts a number of seconds to the recognized time-shift time, positive or negative. A single integer applies to both begin and end times; `a,b` applies `a` to the begin time and `b` to the end time. Commonly used to compensate for clock drift on the upstream server, or to shift playback begin/end boundaries independently. ``` # Shift the entire playseek range later by 1 hour (3600 seconds) ?playseek=20240101120000-20240101130000&r2h-seek-offset=3600 +# Shift the begin time later by 12 seconds and the end time earlier by 12 seconds +?playseek=20240101120000-20240101130000&r2h-seek-offset=12,-12 + # Shift earlier by 30 seconds ?playseek=20240101120000&r2h-seek-offset=-30 ``` @@ -70,7 +73,7 @@ Adds or subtracts a number of seconds to the recognized time-shift time, positiv > [!IMPORTANT] > `r2h-seek-offset` is a "manual time shift", not a timezone correction. It is **always** applied to the final result, even when the input time already carries its own timezone (e.g. ISO 8601 `Z` suffix, `yyyyMMddHHmmssGMT`). > -> In Range Seek mode the offset also enters the window check — once the offset-adjusted time falls outside the window, it likewise falls back to passthrough. +> In Range Seek mode the offset also enters the window check — once the offset-adjusted begin time falls outside the window, it likewise falls back to passthrough. With the `a,b` form, Range Seek only uses the begin time, so only `a` affects the window check and the `Range: clock=` header. ### r2h-seek-mode (optional, RTSP only) diff --git a/docs/en/guide/url-formats.md b/docs/en/guide/url-formats.md index cf5866b..2937607 100644 --- a/docs/en/guide/url-formats.md +++ b/docs/en/guide/url-formats.md @@ -62,8 +62,8 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=202401011200 # Time-shifted playback (using tvdr parameter) http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?tvdr=20240101120000GMT-20240101130000GMT -# Custom time-shift parameter name + time offset -http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?seek=20240101120000&r2h-seek-name=seek&r2h-seek-offset=3600 +# Custom time-shift parameter name + independent begin/end offsets +http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?seek=20240101120000-20240101130000&r2h-seek-name=seek&r2h-seek-offset=12,-12 # Explicitly enable the RTSP near-realtime optimization (see r2h-seek-mode docs) http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=20240101120000&r2h-seek-mode=range(UTC%2B8/3600) @@ -85,7 +85,7 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?r2h-ifname=eth0 This is caused by timezone mismatch. You need to perform timezone conversion. Try the following methods: - Modify the player's User-Agent setting by adding `TZ/UTC+8` or `TZ/UTC-8`. For example, `AptvPlayer/1.3.3 TZ/UTC+8`. -- Modify the playback URL by adding the parameter `&r2h-seek-offset=28800` or `&r2h-seek-offset=-28800` +- Modify the playback URL by adding the parameter `&r2h-seek-offset=28800` or `&r2h-seek-offset=-28800`. If you only need to adjust the begin/end boundaries, use a form like `&r2h-seek-offset=12,-12` to offset them separately For detailed information on time-shift parameter handling (timezone, offset), see [Time Processing Guide](/en/guide/time-processing). diff --git a/docs/guide/time-processing.md b/docs/guide/time-processing.md index 3de0f54..1b503dd 100644 --- a/docs/guide/time-processing.md +++ b/docs/guide/time-processing.md @@ -57,12 +57,15 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=202401011200 ### r2h-seek-offset(可选) -对识别出的时移时间额外加 / 减若干秒,可正可负。常用于补偿上游服务器的时钟偏差,或整体提前 / 延后开始时间。 +对识别出的时移时间额外加 / 减若干秒,可正可负。传单个整数时同时作用于起始和结束时间;传 `a,b` 时分别作用于起始时间和结束时间。常用于补偿上游服务器的时钟偏差,或让起播/结束边界分别提前、延后。 ``` # playseek 范围整体后移 1 小时(3600 秒) ?playseek=20240101120000-20240101130000&r2h-seek-offset=3600 +# 起始时间后移 12 秒,结束时间提前 12 秒 +?playseek=20240101120000-20240101130000&r2h-seek-offset=12,-12 + # 提前 30 秒 ?playseek=20240101120000&r2h-seek-offset=-30 ``` @@ -70,7 +73,7 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=202401011200 > [!IMPORTANT] > `r2h-seek-offset` 是「人为时间平移」,不是时区修正。它**总是**叠加到最终结果上,即使输入时间已经自带时区(如 ISO 8601 `Z` 后缀、`yyyyMMddHHmmssGMT`),仍然生效。 > -> 在 Range Seek 模式下,offset 也会进入窗口判定——offset 后的时间一旦落出窗口,同样回退为透传。 +> 在 Range Seek 模式下,offset 也会进入窗口判定——offset 后的起始时间一旦落出窗口,同样回退为透传。使用 `a,b` 形式时,Range Seek 只使用起始时间,因此只有 `a` 会影响窗口判定和 `Range: clock=` 头。 ### r2h-seek-mode(可选,仅 RTSP) diff --git a/docs/guide/url-formats.md b/docs/guide/url-formats.md index b73d64d..8922512 100644 --- a/docs/guide/url-formats.md +++ b/docs/guide/url-formats.md @@ -62,8 +62,8 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=202401011200 # 时移回看(使用 tvdr 参数) http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?tvdr=20240101120000GMT-20240101130000GMT -# 自定义时移参数名 + 时间偏移 -http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?seek=20240101120000&r2h-seek-name=seek&r2h-seek-offset=3600 +# 自定义时移参数名 + 起止时间独立偏移 +http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?seek=20240101120000-20240101130000&r2h-seek-name=seek&r2h-seek-offset=12,-12 # 显式开启 RTSP 近实时优化(参见 r2h-seek-mode 文档) http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?playseek=20240101120000&r2h-seek-mode=range(UTC%2B8/3600) @@ -85,7 +85,7 @@ http://192.168.1.1:5140/rtsp/iptv.example.com:554/channel1?r2h-ifname=eth0 这是由于时区未能匹配。需要做时区转换。你可以尝试以下几种方式。 - 修改播放器 User Agent 设置,加上 `TZ/UTC+8` 或 `TZ/UTC-8`。例如 `AptvPlayer/1.3.3 TZ/UTC+8`。 -- 修改播放链接,加上参数 `&r2h-seek-offset=28800` 或 `&r2h-seek-offset=-28800` +- 修改播放链接,加上参数 `&r2h-seek-offset=28800` 或 `&r2h-seek-offset=-28800`。如果只需要调整起始/结束边界,可以使用 `&r2h-seek-offset=12,-12` 这样的形式分别偏移起始和结束时间 关于时移回看的参数处理(时区、偏移),详见 [时间处理说明](./time-processing.md)。 diff --git a/e2e/test_rtsp_seek_mode.py b/e2e/test_rtsp_seek_mode.py index 1c85f8e..8e03e00 100644 --- a/e2e/test_rtsp_seek_mode.py +++ b/e2e/test_rtsp_seek_mode.py @@ -369,6 +369,29 @@ def test_range_offset_propagates_into_clock(self, shared_r2h): finally: rtsp.stop() + def test_range_offset_pair_uses_begin_for_clock(self, shared_r2h): + """r2h-seek-offset=a,b should use begin offset for the clock= header.""" + rtsp = MockRTSPServer(num_packets=500) + rtsp.start() + try: + base_ts = int(time.time()) - 3000 # 50 min ago + begin_offset = 1800 # +30 min, still within 60min window relative to begin + end_offset = -1800 + cst_str = _format_yyyyMMddHHmmss(base_ts + 8 * 3600) + url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-offset=%d,%d&r2h-seek-mode=range(UTC%%2B8/3600)" % ( + rtsp.port, + cst_str, + begin_offset, + end_offset, + ) + + stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) + + play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] + assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(base_ts + begin_offset) + finally: + rtsp.stop() + def test_range_offset_pushes_outside_window_falls_back(self, shared_r2h): """If r2h-seek-offset shifts begin out of the window, fall back to passthrough.""" rtsp = MockRTSPServer(num_packets=500) @@ -570,6 +593,33 @@ def test_request_seek_offset_overrides_configured(self, r2h_binary): finally: rtsp.stop() + def test_configured_seek_offset_pair_fallback(self, r2h_binary): + """Configured r2h-seek-offset=a,b should be used when the request omits it.""" + r2h_port = find_free_port() + rtsp = MockRTSPServer(num_packets=500) + rtsp.start() + try: + config = make_m3u_rtsp_config(r2h_port, rtsp.port, "OffsetPairMerge", "?r2h-seek-offset=30,-60") + r2h = R2HProcess(r2h_binary, r2h_port, config_content=config) + r2h.start() + try: + url = "/OffsetPairMerge?playseek=20240101120000-20240101130000" + + stream_get("127.0.0.1", r2h_port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) + + describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] + assert describe_reqs, "expected at least one DESCRIBE" + assert "r2h-seek-offset" not in describe_reqs[0]["uri"], ( + "r2h-seek-offset leaked into upstream URI: %s" % describe_reqs[0]["uri"] + ) + assert "playseek=20240101120030-20240101125900" in describe_reqs[0]["uri"], ( + "configured offset pair should have applied; got URI %s" % describe_reqs[0]["uri"] + ) + finally: + r2h.stop() + finally: + rtsp.stop() + def test_duplicate_request_seek_mode_does_not_leak(self, shared_r2h): """A client that sends r2h-seek-mode twice must have BOTH copies stripped from the upstream URI — the second copy could otherwise be re-parsed diff --git a/e2e/test_url_template.py b/e2e/test_url_template.py index 9cb00d9..d771cfe 100644 --- a/e2e/test_url_template.py +++ b/e2e/test_url_template.py @@ -566,6 +566,39 @@ def test_seek_offset_applied(self, shared_r2h): finally: upstream.stop() + def test_seek_offset_pair_applied(self, shared_r2h): + """r2h-seek-offset=a,b should shift template begin and end independently.""" + # begin + 30s = 12:00:30, end - 60s = 12:59:00 + expected_path = "/path/20240101120030/20240101125900/file.m3u8" + upstream = _make_upstream(expected_path) + upstream.start() + try: + url = ( + "/http/127.0.0.1:%d" + "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" + "?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" + ) % upstream.port + status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + assert status == 200 + assert _get_upstream_path(upstream) == expected_path + finally: + upstream.stop() + + def test_seek_offset_pair_begin_only_template(self, shared_r2h): + """When seek has no end time, r2h-seek-offset=a,b should use only begin offset.""" + expected_path = "/path/20240101120030/file.m3u8" + upstream = _make_upstream(expected_path) + upstream.start() + try: + url = ( + "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file.m3u8?playseek=20240101120000&r2h-seek-offset=30,-60" + ) % upstream.port + status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + assert status == 200 + assert _get_upstream_path(upstream) == expected_path + finally: + upstream.stop() + def test_tz_and_offset_combined(self, shared_r2h): """TZ/UTC+8 + r2h-seek-offset=3600: 12:00 CST+1h=13:00 CST, end 14:00 CST (local time).""" expected_path = "/path/20240101130000/20240101140000/file.m3u8" @@ -730,6 +763,23 @@ def test_seek_offset_with_unix_timestamp_seek(self, shared_r2h): finally: upstream.stop() + def test_seek_offset_pair_with_unix_timestamp_seek(self, shared_r2h): + """r2h-seek-offset=a,b should shift Unix timestamp begin/end independently.""" + expected_path = "/path/20240101120030/20240101125900/file" + upstream = _make_upstream(expected_path) + upstream.start() + try: + url = ( + "/http/127.0.0.1:%d" + "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" + "?playseek=%d-%d&r2h-seek-offset=30,-60" + ) % (upstream.port, _BEGIN_EPOCH, _END_EPOCH) + status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + assert status == 200 + assert _get_upstream_path(upstream) == expected_path + finally: + upstream.stop() + # =================================================================== # Edge cases @@ -1189,6 +1239,21 @@ def test_positive_offset(self, shared_r2h): finally: upstream.stop() + def test_offset_pair(self, shared_r2h): + """r2h-seek-offset=a,b should add separate begin/end offsets.""" + upstream = _make_upstream("/stream") + upstream.start() + try: + url = ( + "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" + ) % upstream.port + http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + + path = _get_upstream_path(upstream) + assert _extract_query_param(path, "playseek") == "20240101120030-20240101125900" + finally: + upstream.stop() + def test_negative_offset(self, shared_r2h): """r2h-seek-offset=-30 should subtract 30 seconds.""" upstream = _make_upstream("/stream") @@ -1232,6 +1297,19 @@ def test_offset_with_unix_timestamp(self, shared_r2h): finally: upstream.stop() + def test_offset_pair_with_unix_timestamp(self, shared_r2h): + """r2h-seek-offset=a,b should preserve Unix timestamp output format.""" + upstream = _make_upstream("/stream") + upstream.start() + try: + url = ("/http/127.0.0.1:%d/stream?playseek=1704096000-1704099600&r2h-seek-offset=30,-60") % upstream.port + http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + + path = _get_upstream_path(upstream) + assert _extract_query_param(path, "playseek") == "1704096030-1704099540" + finally: + upstream.stop() + @pytest.mark.http_proxy class TestHTTPQueryAppendTimezoneAndFormat: @@ -2336,6 +2414,27 @@ def test_positive_offset(self, shared_r2h): finally: rtsp.stop() + def test_offset_pair(self, shared_r2h): + """r2h-seek-offset=a,b should add separate begin/end offsets in RTSP.""" + rtsp = MockRTSPServer(num_packets=500) + rtsp.start() + try: + url = ( + "/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" + ) % rtsp.port + stream_get( + "127.0.0.1", + shared_r2h.port, + url, + read_bytes=4096, + timeout=_STREAM_TIMEOUT, + ) + + uri = _get_describe_uri(rtsp) + assert _extract_query_param(uri, "playseek") == "20240101120030-20240101125900" + finally: + rtsp.stop() + def test_negative_offset(self, shared_r2h): """r2h-seek-offset=-30 should subtract 30 seconds from RTSP playseek.""" rtsp = MockRTSPServer(num_packets=500) diff --git a/src/service.c b/src/service.c index 775365a..fc4f3ab 100644 --- a/src/service.c +++ b/src/service.c @@ -400,6 +400,52 @@ static int parse_seek_mode_value(const char *value, seek_mode_t *out_mode, int * return 0; } +static int parse_seek_offset_component(const char *value, int *out_offset_seconds) { + char *endptr; + long offset_val; + + if (!value || !out_offset_seconds || value[0] == '\0') + return -1; + + offset_val = strtol(value, &endptr, 10); + if (endptr == value || *endptr != '\0' || offset_val < INT_MIN || offset_val > INT_MAX) + return -1; + + *out_offset_seconds = (int)offset_val; + return 0; +} + +static int parse_seek_offset_value(char *value, int *out_begin_offset_seconds, int *out_end_offset_seconds) { + int begin_offset_seconds; + int end_offset_seconds; + char *comma; + + if (!value || !out_begin_offset_seconds || !out_end_offset_seconds) + return -1; + + comma = strchr(value, ','); + if (!comma) { + if (parse_seek_offset_component(value, &begin_offset_seconds) != 0) + return -1; + *out_begin_offset_seconds = begin_offset_seconds; + *out_end_offset_seconds = begin_offset_seconds; + return 0; + } + + if (strchr(comma + 1, ',')) + return -1; + + *comma = '\0'; + if (parse_seek_offset_component(value, &begin_offset_seconds) != 0 || + parse_seek_offset_component(comma + 1, &end_offset_seconds) != 0) { + return -1; + } + + *out_begin_offset_seconds = begin_offset_seconds; + *out_end_offset_seconds = end_offset_seconds; + return 0; +} + /** * Find a query parameter, copy its URL-decoded value into the caller's buffer, * and remove the parameter from the query string in place. If the parameter is @@ -464,14 +510,15 @@ static int extract_query_param(char **query_start_ptr, const char *param_name, c } int service_extract_seek_params(char *query_start, char **out_seek_param_name, char **out_seek_param_value, - int *out_seek_offset_seconds, seek_mode_t *out_seek_mode, - int *out_seek_mode_tz_explicit, int *out_seek_mode_tz_offset_seconds, - int *out_seek_mode_window_seconds) { + int *out_seek_begin_offset_seconds, int *out_seek_end_offset_seconds, + seek_mode_t *out_seek_mode, int *out_seek_mode_tz_explicit, + int *out_seek_mode_tz_offset_seconds, int *out_seek_mode_window_seconds) { char r2h_seek_name_buf[128]; int has_seek_name = 0; const char *seek_param_name = NULL; char *seek_param_value = NULL; - int seek_offset_seconds = 0; + int seek_begin_offset_seconds = 0; + int seek_end_offset_seconds = 0; seek_mode_t seek_mode = SEEK_MODE_PASSTHROUGH; int seek_mode_tz_explicit = 0; int seek_mode_tz_offset_seconds = 0; @@ -479,14 +526,15 @@ int service_extract_seek_params(char *query_start, char **out_seek_param_name, c char heuristic_seek_name[16]; if (!query_start || *query_start != '?' || !out_seek_param_name || !out_seek_param_value || - !out_seek_offset_seconds || !out_seek_mode || !out_seek_mode_tz_explicit || !out_seek_mode_tz_offset_seconds || - !out_seek_mode_window_seconds) { + !out_seek_begin_offset_seconds || !out_seek_end_offset_seconds || !out_seek_mode || !out_seek_mode_tz_explicit || + !out_seek_mode_tz_offset_seconds || !out_seek_mode_window_seconds) { return -1; } *out_seek_param_name = NULL; *out_seek_param_value = NULL; - *out_seek_offset_seconds = 0; + *out_seek_begin_offset_seconds = 0; + *out_seek_end_offset_seconds = 0; *out_seek_mode = SEEK_MODE_PASSTHROUGH; *out_seek_mode_tz_explicit = 0; *out_seek_mode_tz_offset_seconds = 0; @@ -499,13 +547,18 @@ int service_extract_seek_params(char *query_start, char **out_seek_param_name, c char offset_buf[32]; if (extract_query_param(&query_start, "r2h-seek-offset", offset_buf, sizeof(offset_buf)) == 1) { - char *endptr; - long offset_val = strtol(offset_buf, &endptr, 10); - if (*endptr == '\0' && offset_val >= INT_MIN && offset_val <= INT_MAX) { - seek_offset_seconds = (int)offset_val; - logger(LOG_DEBUG, "Found r2h-seek-offset parameter: %d seconds", seek_offset_seconds); + char offset_log_buf[sizeof(offset_buf)]; + strncpy(offset_log_buf, offset_buf, sizeof(offset_log_buf) - 1); + offset_log_buf[sizeof(offset_log_buf) - 1] = '\0'; + if (parse_seek_offset_value(offset_buf, &seek_begin_offset_seconds, &seek_end_offset_seconds) == 0) { + if (seek_begin_offset_seconds == seek_end_offset_seconds) { + logger(LOG_DEBUG, "Found r2h-seek-offset parameter: %d seconds", seek_begin_offset_seconds); + } else { + logger(LOG_DEBUG, "Found r2h-seek-offset parameter: begin %+d seconds, end %+d seconds", + seek_begin_offset_seconds, seek_end_offset_seconds); + } } else { - logger(LOG_WARN, "Invalid r2h-seek-offset value: %s", offset_buf); + logger(LOG_WARN, "Invalid r2h-seek-offset value: %s", offset_log_buf); } } @@ -655,7 +708,8 @@ int service_extract_seek_params(char *query_start, char **out_seek_param_name, c *out_seek_param_name = strdup(seek_param_name); *out_seek_param_value = seek_param_value; seek_param_value = NULL; /* Transfer ownership */ - *out_seek_offset_seconds = seek_offset_seconds; + *out_seek_begin_offset_seconds = seek_begin_offset_seconds; + *out_seek_end_offset_seconds = seek_end_offset_seconds; *out_seek_mode = seek_mode; *out_seek_mode_tz_explicit = seek_mode_tz_explicit; *out_seek_mode_tz_offset_seconds = seek_mode_tz_offset_seconds; @@ -758,16 +812,18 @@ static const char *find_seek_range_separator(const char *value) { return NULL; } -int service_parse_seek_value(const char *seek_param_value, int seek_offset_seconds, const char *user_agent, - seek_mode_t seek_mode, int seek_mode_tz_explicit, int seek_mode_tz_offset_seconds, - int seek_mode_window_seconds, seek_parse_result_t *parse_result) { +int service_parse_seek_value(const char *seek_param_value, int seek_begin_offset_seconds, int seek_end_offset_seconds, + const char *user_agent, seek_mode_t seek_mode, int seek_mode_tz_explicit, + int seek_mode_tz_offset_seconds, int seek_mode_window_seconds, + seek_parse_result_t *parse_result) { const char *dash_pos; if (!parse_result) return -1; memset(parse_result, 0, sizeof(*parse_result)); - parse_result->seek_offset_seconds = seek_offset_seconds; + parse_result->seek_begin_offset_seconds = seek_begin_offset_seconds; + parse_result->seek_end_offset_seconds = seek_end_offset_seconds; parse_result->now_utc = time(NULL); if (user_agent) @@ -800,12 +856,12 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon } if (parse_result->has_begin && timezone_parse_to_utc(parse_result->begin_str, parse_result->tz_offset_seconds, - seek_offset_seconds, &parse_result->begin_utc) == 0) { + seek_begin_offset_seconds, &parse_result->begin_utc) == 0) { parse_result->begin_parsed = 1; } if (parse_result->has_end && timezone_parse_to_utc(parse_result->end_str, parse_result->tz_offset_seconds, - seek_offset_seconds, &parse_result->end_utc) == 0) { + seek_end_offset_seconds, &parse_result->end_utc) == 0) { parse_result->end_parsed = 1; } @@ -837,7 +893,7 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon /* Recent-clock optimization: opt-in via r2h-seek-mode=range(...). The TZ used * for the recency comparison may differ from the passthrough TZ when range() - * supplies an explicit TZ that overrides the UA TZ. r2h-seek-offset is baked + * supplies an explicit TZ that overrides the UA TZ. The begin r2h-seek-offset is baked * into begin_utc via timezone_parse_to_utc above and propagates here. * * The recent-clock UTC time is stored separately in recent_clock_tm_utc so @@ -849,7 +905,7 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon time_t begin_utc_for_recent; int recompute = seek_mode_tz_explicit && seek_mode_tz_offset_seconds != parse_result->tz_offset_seconds; if (recompute) { - if (timezone_parse_to_utc(parse_result->begin_str, seek_mode_tz_offset_seconds, seek_offset_seconds, + if (timezone_parse_to_utc(parse_result->begin_str, seek_mode_tz_offset_seconds, seek_begin_offset_seconds, &begin_utc_for_recent) != 0) { return 0; } @@ -882,7 +938,7 @@ int service_convert_seek_value(const seek_parse_result_t *parse_result, char *ou if (parse_result->has_begin && timezone_convert_time_with_offset(parse_result->begin_str, parse_result->tz_offset_seconds, - parse_result->seek_offset_seconds, begin_utc, sizeof(begin_utc)) == 0) { + parse_result->seek_begin_offset_seconds, begin_utc, sizeof(begin_utc)) == 0) { logger(LOG_DEBUG, "Converted begin time '%s' to UTC '%s'", parse_result->begin_str, begin_utc); } else { strncpy(begin_utc, parse_result->begin_str, sizeof(begin_utc) - 1); @@ -891,7 +947,7 @@ int service_convert_seek_value(const seek_parse_result_t *parse_result, char *ou if (parse_result->has_end) { if (timezone_convert_time_with_offset(parse_result->end_str, parse_result->tz_offset_seconds, - parse_result->seek_offset_seconds, end_utc, sizeof(end_utc)) == 0) { + parse_result->seek_end_offset_seconds, end_utc, sizeof(end_utc)) == 0) { logger(LOG_DEBUG, "Converted end time '%s' to UTC '%s'", parse_result->end_str, end_utc); } else { strncpy(end_utc, parse_result->end_str, sizeof(end_utc) - 1); @@ -1172,7 +1228,8 @@ service_t *service_create_from_http_url(const char *http_url) { char *query_start = strchr(result->http_url, '?'); if (query_start) { service_extract_seek_params(query_start, &result->seek_param_name, &result->seek_param_value, - &result->seek_offset_seconds, &result->seek_mode, &result->seek_mode_tz_explicit, + &result->seek_begin_offset_seconds, &result->seek_end_offset_seconds, + &result->seek_mode, &result->seek_mode_tz_explicit, &result->seek_mode_tz_offset_seconds, &result->seek_mode_window_seconds); service_extract_ifname_params(query_start, &result->ifname, &result->ifname_fcc); service_strip_query_param(query_start, "r2h-token"); @@ -1224,7 +1281,8 @@ service_t *service_create_from_rtsp_url(const char *http_url) { char rtsp_url[HTTP_URL_BUFFER_SIZE]; char *seek_param_name = NULL; char *seek_param_value = NULL; - int seek_offset_seconds = 0; + int seek_begin_offset_seconds = 0; + int seek_end_offset_seconds = 0; seek_mode_t seek_mode = SEEK_MODE_PASSTHROUGH; int seek_mode_tz_explicit = 0; int seek_mode_tz_offset_seconds = 0; @@ -1258,9 +1316,9 @@ service_t *service_create_from_rtsp_url(const char *http_url) { char *ifname = NULL, *ifname_fcc = NULL; query_start = strchr(url_part, '?'); if (query_start) { - if (service_extract_seek_params(query_start, &seek_param_name, &seek_param_value, &seek_offset_seconds, &seek_mode, - &seek_mode_tz_explicit, &seek_mode_tz_offset_seconds, - &seek_mode_window_seconds) < 0) { + if (service_extract_seek_params(query_start, &seek_param_name, &seek_param_value, &seek_begin_offset_seconds, + &seek_end_offset_seconds, &seek_mode, &seek_mode_tz_explicit, + &seek_mode_tz_offset_seconds, &seek_mode_window_seconds) < 0) { return NULL; } service_extract_ifname_params(query_start, &ifname, &ifname_fcc); @@ -1298,7 +1356,8 @@ service_t *service_create_from_rtsp_url(const char *http_url) { seek_param_name = NULL; /* Transfer ownership */ result->seek_param_value = seek_param_value; seek_param_value = NULL; /* Transfer ownership */ - result->seek_offset_seconds = seek_offset_seconds; + result->seek_begin_offset_seconds = seek_begin_offset_seconds; + result->seek_end_offset_seconds = seek_end_offset_seconds; result->seek_mode = seek_mode; result->seek_mode_tz_explicit = seek_mode_tz_explicit; result->seek_mode_tz_offset_seconds = seek_mode_tz_offset_seconds; @@ -1447,11 +1506,17 @@ service_t *service_create_with_query_merge(service_t *configured_service, const } } - if (configured_service->seek_offset_seconds != 0 && !request_query_has(query_start, "r2h-seek-offset")) { + if ((configured_service->seek_begin_offset_seconds != 0 || configured_service->seek_end_offset_seconds != 0) && + !request_query_has(query_start, "r2h-seek-offset")) { char seek_offset_param[64]; const char *separator = strchr(merged_url, '?') ? "&" : "?"; - snprintf(seek_offset_param, sizeof(seek_offset_param), "%sr2h-seek-offset=%d", separator, - configured_service->seek_offset_seconds); + if (configured_service->seek_begin_offset_seconds == configured_service->seek_end_offset_seconds) { + snprintf(seek_offset_param, sizeof(seek_offset_param), "%sr2h-seek-offset=%d", separator, + configured_service->seek_begin_offset_seconds); + } else { + snprintf(seek_offset_param, sizeof(seek_offset_param), "%sr2h-seek-offset=%d,%d", separator, + configured_service->seek_begin_offset_seconds, configured_service->seek_end_offset_seconds); + } if (strlen(merged_url) + strlen(seek_offset_param) < sizeof(merged_url)) { strcat(merged_url, seek_offset_param); } else { @@ -1899,7 +1964,8 @@ service_t *service_clone(service_t *service) { } } - cloned->seek_offset_seconds = service->seek_offset_seconds; + cloned->seek_begin_offset_seconds = service->seek_begin_offset_seconds; + cloned->seek_end_offset_seconds = service->seek_end_offset_seconds; cloned->seek_mode = service->seek_mode; cloned->seek_mode_tz_explicit = service->seek_mode_tz_explicit; cloned->seek_mode_tz_offset_seconds = service->seek_mode_tz_offset_seconds; diff --git a/src/service.h b/src/service.h index dbebfa3..48b781e 100644 --- a/src/service.h +++ b/src/service.h @@ -69,8 +69,8 @@ typedef struct service_s { char *http_url; /* Full HTTP URL for SERVICE_HTTP */ char *seek_param_name; /* Name of seek parameter (e.g., "playseek", "tvdr") */ char *seek_param_value; /* Value of seek parameter for time range */ - int seek_offset_seconds; /* Additional offset in seconds from r2h-seek-offset - parameter */ + int seek_begin_offset_seconds; /* Begin offset in seconds from r2h-seek-offset parameter */ + int seek_end_offset_seconds; /* End offset in seconds from r2h-seek-offset parameter */ seek_mode_t seek_mode; /* Seek mode from r2h-seek-mode parameter */ int seek_mode_tz_explicit; /* 1 if range(...) explicitly specified a TZ */ int seek_mode_tz_offset_seconds; /* TZ offset from range(TZ/...) when explicit */ @@ -142,7 +142,8 @@ service_t *service_create_from_http_url(const char *http_url); * @param out_seek_param_name Output: malloc'd seek parameter name (caller frees) * @param out_seek_param_value Output: malloc'd seek parameter value (caller * frees) - * @param out_seek_offset_seconds Output: seek offset in seconds + * @param out_seek_begin_offset_seconds Output: begin seek offset in seconds + * @param out_seek_end_offset_seconds Output: end seek offset in seconds * @param out_seek_mode Output: parsed seek mode (default SEEK_MODE_PASSTHROUGH) * @param out_seek_mode_tz_explicit Output: 1 if range(...) explicitly gave a TZ * @param out_seek_mode_tz_offset_seconds Output: TZ offset when explicit @@ -150,15 +151,16 @@ service_t *service_create_from_http_url(const char *http_url); * @return 0 on success, -1 on failure */ int service_extract_seek_params(char *query_start, char **out_seek_param_name, char **out_seek_param_value, - int *out_seek_offset_seconds, seek_mode_t *out_seek_mode, - int *out_seek_mode_tz_explicit, int *out_seek_mode_tz_offset_seconds, - int *out_seek_mode_window_seconds); + int *out_seek_begin_offset_seconds, int *out_seek_end_offset_seconds, + seek_mode_t *out_seek_mode, int *out_seek_mode_tz_explicit, + int *out_seek_mode_tz_offset_seconds, int *out_seek_mode_window_seconds); /** * Analyze a seek parameter once and reuse the result across RTSP/HTTP flows. * * @param seek_param_value Extracted seek parameter value - * @param seek_offset_seconds Additional seek offset in seconds + * @param seek_begin_offset_seconds Additional begin seek offset in seconds + * @param seek_end_offset_seconds Additional end seek offset in seconds * @param user_agent User-Agent header for timezone detection * @param seek_mode Seek mode from r2h-seek-mode parameter * @param seek_mode_tz_explicit 1 if range(...) explicitly specified a TZ @@ -167,9 +169,10 @@ int service_extract_seek_params(char *query_start, char **out_seek_param_name, c * @param parse_result Output parse result structure * @return 0 on success, -1 on invalid parameters */ -int service_parse_seek_value(const char *seek_param_value, int seek_offset_seconds, const char *user_agent, - seek_mode_t seek_mode, int seek_mode_tz_explicit, int seek_mode_tz_offset_seconds, - int seek_mode_window_seconds, seek_parse_result_t *parse_result); +int service_parse_seek_value(const char *seek_param_value, int seek_begin_offset_seconds, int seek_end_offset_seconds, + const char *user_agent, seek_mode_t seek_mode, int seek_mode_tz_explicit, + int seek_mode_tz_offset_seconds, int seek_mode_window_seconds, + seek_parse_result_t *parse_result); /** * Convert a parsed seek value to upstream UTC query form. diff --git a/src/stream.c b/src/stream.c index e8998a8..8f52a52 100644 --- a/src/stream.c +++ b/src/stream.c @@ -178,10 +178,10 @@ int stream_context_init_for_worker(stream_context_t *ctx, connection_t *conn, se return -1; } - if (service_parse_seek_value(service->seek_param_value, service->seek_offset_seconds, service->user_agent, - service->seek_mode, service->seek_mode_tz_explicit, - service->seek_mode_tz_offset_seconds, service->seek_mode_window_seconds, - &seek_parse_result) != 0) { + if (service_parse_seek_value(service->seek_param_value, service->seek_begin_offset_seconds, + service->seek_end_offset_seconds, service->user_agent, service->seek_mode, + service->seek_mode_tz_explicit, service->seek_mode_tz_offset_seconds, + service->seek_mode_window_seconds, &seek_parse_result) != 0) { logger(LOG_ERROR, "HTTP Proxy: Failed to parse seek parameters"); return -1; } @@ -259,10 +259,10 @@ int stream_context_init_for_worker(stream_context_t *ctx, connection_t *conn, se return -1; } - if (service_parse_seek_value(service->seek_param_value, service->seek_offset_seconds, service->user_agent, - service->seek_mode, service->seek_mode_tz_explicit, - service->seek_mode_tz_offset_seconds, service->seek_mode_window_seconds, - &seek_parse_result) != 0) { + if (service_parse_seek_value(service->seek_param_value, service->seek_begin_offset_seconds, + service->seek_end_offset_seconds, service->user_agent, service->seek_mode, + service->seek_mode_tz_explicit, service->seek_mode_tz_offset_seconds, + service->seek_mode_window_seconds, &seek_parse_result) != 0) { logger(LOG_ERROR, "RTSP: Failed to parse seek parameters"); return -1; } diff --git a/src/url_template.h b/src/url_template.h index dc680f8..09c499a 100644 --- a/src/url_template.h +++ b/src/url_template.h @@ -12,7 +12,8 @@ typedef struct seek_parse_result_s { int begin_parsed; int end_parsed; int tz_offset_seconds; - int seek_offset_seconds; + int seek_begin_offset_seconds; + int seek_end_offset_seconds; int is_recent; time_t now_utc; time_t begin_utc; From 9bd1b5a7d4c0b6912348f90299baeb54fc808015 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Fri, 12 Jun 2026 13:23:44 +0800 Subject: [PATCH 2/2] fix(seek): reject overflowing seek offsets --- e2e/test_url_template.py | 17 +++++++++++++++++ src/service.c | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/e2e/test_url_template.py b/e2e/test_url_template.py index d771cfe..b772020 100644 --- a/e2e/test_url_template.py +++ b/e2e/test_url_template.py @@ -1269,6 +1269,23 @@ def test_negative_offset(self, shared_r2h): finally: upstream.stop() + def test_overflow_offset_rejected(self, shared_r2h): + """Overflowing r2h-seek-offset should be ignored.""" + upstream = _make_upstream("/stream") + upstream.start() + try: + url = ( + "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000" + "&r2h-seek-offset=999999999999999999999999999999" + ) % upstream.port + http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) + + path = _get_upstream_path(upstream) + assert _extract_query_param(path, "playseek") == "20240101120000-20240101130000" + assert "r2h-seek-offset" not in path, "r2h-seek-offset should be stripped, got: %s" % path + finally: + upstream.stop() + def test_offset_stripped_from_upstream(self, shared_r2h): """r2h-seek-offset should be stripped from the upstream URL.""" upstream = _make_upstream("/stream") diff --git a/src/service.c b/src/service.c index fc4f3ab..49eff8c 100644 --- a/src/service.c +++ b/src/service.c @@ -5,6 +5,7 @@ #include "timezone.h" #include "url_template.h" #include "utils.h" +#include #include #include #include @@ -407,8 +408,9 @@ static int parse_seek_offset_component(const char *value, int *out_offset_second if (!value || !out_offset_seconds || value[0] == '\0') return -1; + errno = 0; offset_val = strtol(value, &endptr, 10); - if (endptr == value || *endptr != '\0' || offset_val < INT_MIN || offset_val > INT_MAX) + if (errno == ERANGE || endptr == value || *endptr != '\0' || offset_val < INT_MIN || offset_val > INT_MAX) return -1; *out_offset_seconds = (int)offset_val;