Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/en/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Unix socket listen paths must be absolute and must not contain whitespace. At st
- `-s, --status-page-path <path>` - Status page and API root path (default: /status)
- `-p, --player-page-path <path>` - Built-in player page path (default: /player)
- `--app-path-prefix <path>` - Public access prefix for all HTTP resources (default: none)
- `--use-relative-path-in-m3u` - Use root-relative URLs when generating playlist.m3u or rewriting M3U through the HTTP proxy (default: disabled)

### Compatibility

Expand Down Expand Up @@ -165,6 +166,12 @@ player-page-path = /player
# prefix, for example /app/rtp2httpd/player.
app-path-prefix = /app/rtp2httpd

# Use root-relative paths in M3U output (default: no)
# When enabled, playlist.m3u and M3U rewritten through the HTTP proxy
# omit the http://host prefix and keep only paths starting with / or app-path-prefix,
# for example /app/rtp2httpd/rtp/...
use-relative-path-in-m3u = no

# Upstream network interface configuration (optional)
#
# Simple configuration: Configure only one default interface for all traffic types
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白
- `-s, --status-page-path <路径>` - 状态页面与 API 根路径 (默认: /status)
- `-p, --player-page-path <路径>` - 内置播放器页面路径 (默认: /player)
- `--app-path-prefix <路径>` - 所有 HTTP 资源的公开访问前缀 (默认: 无)
- `--use-relative-path-in-m3u` - 生成 playlist.m3u 或 HTTP 代理改写 M3U 时使用站点根相对 URL (默认: 关闭)

### 兼容性

Expand Down Expand Up @@ -164,6 +165,11 @@ player-page-path = /player
# 都会在此前缀下提供,例如 /app/rtp2httpd/player。
app-path-prefix = /app/rtp2httpd

# M3U 输出使用站点根相对路径(默认: no)
# 开启后,playlist.m3u 和 HTTP 代理改写后的 M3U 中不会包含 http://host 前缀,
# 只保留 / 或 app-path-prefix 开头的路径,例如 /app/rtp2httpd/rtp/...
use-relative-path-in-m3u = no

# 上游网络接口配置 (可选)
#
# 简单配置:只配置一个默认接口,所有流量类型都使用此接口
Expand Down
27 changes: 27 additions & 0 deletions e2e/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@ def _assert_xff_enabled(port: int, expected_host: str):
assert expected_host in body.decode()


def _assert_use_relative_path_in_m3u(port: int, expected_enabled: bool):
status, _, body = http_get("127.0.0.1", port, "/playlist.m3u")
text = body.decode()

assert status == 200
assert "Config Test" in text
if expected_enabled:
assert "http://" not in text
assert "/Config%20Test" in text
else:
assert "http://127.0.0.1:" in text


def _assert_udpxy_state(port: int, expected_enabled: bool):
status, _, _ = http_request(
"127.0.0.1",
Expand Down Expand Up @@ -305,6 +318,20 @@ def _assert_http_proxy_user_agent(port: int, expected_user_agent: str):
},
id="udpxy",
),
pytest.param(
{
"name": "use-relative-path-in-m3u",
"config_lines": _bool_config_line("use-relative-path-in-m3u"),
"cli_args": _enable_flag("--use-relative-path-in-m3u"),
"config_source_value": True,
"cli_source_value": True,
"priority_config_value": False,
"priority_cli_value": True,
"assertion": _assert_use_relative_path_in_m3u,
"services_content": _INLINE_M3U,
},
id="use-relative-path-in-m3u",
),
pytest.param(
{
"name": "http-proxy-user-agent",
Expand Down
39 changes: 39 additions & 0 deletions e2e/test_http_proxy_m3u_rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ def prefixed_r2h(r2h_binary):
r2h.stop()


@pytest.fixture(scope="module")
def relative_path_prefixed_r2h(r2h_binary):
"""A shared rtp2httpd instance with app-path-prefix and relative M3U URLs."""
port = find_free_port()
config = f"""\
[global]
verbosity = 4
maxclients = 100
app-path-prefix = {APP_PREFIX}
use-relative-path-in-m3u = yes

[bind]
* {port}
"""
r2h = R2HProcess(r2h_binary, port, config_content=config)
r2h.start()
yield r2h
r2h.stop()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -215,6 +235,25 @@ def test_segment_urls_rewritten_with_app_path_prefix(self, prefixed_r2h):
finally:
upstream.stop()

def test_segment_urls_rewritten_as_relative_with_app_path_prefix(self, relative_path_prefixed_r2h):
"""Relative M3U mode should omit scheme/host and keep app-path-prefix."""
m3u = "#EXTM3U\n#EXT-X-TARGETDURATION:10\n#EXTINF:10,\nhttp://10.0.0.1:8080/seg1.ts\n"
upstream = _make_m3u_upstream("/live/playlist.m3u8", m3u)
try:
status, _, text = _m3u_get(
relative_path_prefixed_r2h,
upstream.port,
"/live/playlist.m3u8",
path_prefix=APP_PREFIX,
)
urls = [line for line in text.splitlines() if "seg1.ts" in line]

assert status == 200
assert urls == [f"{APP_PREFIX}/http/10.0.0.1:8080/seg1.ts"]
assert "http://" not in text
finally:
upstream.stop()

def test_variant_playlist_urls_rewritten(self, shared_r2h):
"""http:// URLs in a master/variant playlist should be rewritten."""
m3u = (
Expand Down
32 changes: 32 additions & 0 deletions e2e/test_m3u.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,38 @@ def test_http_url_preserved_or_rewritten(self, r2h_binary):
finally:
r2h.stop()

def test_relative_path_output_with_app_path_prefix(self, r2h_binary):
"""When enabled, playlist URLs should omit scheme/host and keep app-path-prefix."""
port = find_free_port()
app_prefix = "/app/rtp2httpd"
config = f"""\
[global]
verbosity = 4
app-path-prefix = {app_prefix}
use-relative-path-in-m3u = yes

[bind]
* {port}

[services]
#EXTM3U
#EXTINF:-1,Relative Channel
rtp://239.0.0.1:1234
"""
r2h = R2HProcess(r2h_binary, port, config_content=config)
try:
r2h.start()
status, _, body = http_get("127.0.0.1", port, f"{app_prefix}/playlist.m3u")
text = body.decode()
urls = [line for line in text.splitlines() if "Relative%20Channel" in line]

assert status == 200
assert len(urls) == 1
assert urls[0] == f"{app_prefix}/Relative%20Channel"
assert "http://" not in text
finally:
r2h.stop()


# ---------------------------------------------------------------------------
# ETag and conditional GET
Expand Down
13 changes: 13 additions & 0 deletions ikuai-support/rtp2httpd/app/option.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@
"min": 0,
"max": 1
},
{
"default": 0,
"attrname": "RTP2HTTPD_USE_RELATIVE_PATH_IN_M3U",
"label": {
"en": "Use root-relative paths in M3U playlists (0=off 1=on)",
"zh": "M3U 播放列表使用站点相对路径(0=关闭 1=开启)"
},
"required": true,
"scope": "config",
"type": "integer",
"min": 0,
"max": 1
},
{
"default": 1,
"attrname": "RTP2HTTPD_WORKERS",
Expand Down
4 changes: 4 additions & 0 deletions ikuai-support/rtp2httpd/scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ load_env_file "$RUNTIME_ENV"
: "${RTP2HTTPD_R2H_TOKEN:=}"
: "${RTP2HTTPD_CORS_ALLOW_ORIGIN:=}"
: "${RTP2HTTPD_XFF:=0}"
: "${RTP2HTTPD_USE_RELATIVE_PATH_IN_M3U:=0}"
: "${RTP2HTTPD_HTTP_PROXY_USER_AGENT:=}"
: "${RTP2HTTPD_RTSP_USER_AGENT:=}"
: "${RTP2HTTPD_EXTRA_ARGS:=}"
Expand Down Expand Up @@ -167,6 +168,9 @@ fi
if [ "$RTP2HTTPD_XFF" = "1" ]; then
set -- "$@" --xff
fi
if [ "$RTP2HTTPD_USE_RELATIVE_PATH_IN_M3U" = "1" ]; then
set -- "$@" --use-relative-path-in-m3u
fi
if [ -n "$RTP2HTTPD_HTTP_PROXY_USER_AGENT" ]; then
set -- "$@" --http-proxy-user-agent "$RTP2HTTPD_HTTP_PROXY_USER_AGENT"
fi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,18 @@ return view.extend({
o.placeholder = "/app/rtp2httpd";
o.depends("use_config_file", "0");

o = s.taboption(
"advanced",
form.Flag,
"use_relative_path_in_m3u",
_("Use Relative Paths in M3U"),
_(
"When enabled, generated and rewritten M3U playlists omit the http://host prefix and use root-relative paths."
)
);
o.default = "0";
o.depends("use_config_file", "0");

o = s.taboption(
"advanced",
form.Value,
Expand Down
8 changes: 8 additions & 0 deletions openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,14 @@ msgstr ""
msgid "Public mount path prefix for all rtp2httpd HTTP resources, for example /app/rtp2httpd."
msgstr ""

#: htdocs/luci-static/resources/view/rtp2httpd.js:760
msgid "Use Relative Paths in M3U"
msgstr ""

#: htdocs/luci-static/resources/view/rtp2httpd.js:762
msgid "When enabled, generated and rewritten M3U playlists omit the http://host prefix and use root-relative paths."
msgstr ""

#: htdocs/luci-static/resources/view/rtp2httpd.js:706
msgid "R2H Token"
msgstr ""
Expand Down
8 changes: 8 additions & 0 deletions openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ msgstr "请先配置外部 M3U URL"
msgid "Public mount path prefix for all rtp2httpd HTTP resources, for example /app/rtp2httpd."
msgstr "所有 rtp2httpd HTTP 资源的公开挂载路径前缀,例如 /app/rtp2httpd。"

#: htdocs/luci-static/resources/view/rtp2httpd.js:760
msgid "Use Relative Paths in M3U"
msgstr "M3U 使用相对路径"

#: htdocs/luci-static/resources/view/rtp2httpd.js:762
msgid "When enabled, generated and rewritten M3U playlists omit the http://host prefix and use root-relative paths."
msgstr "启用后,生成和改写后的 M3U 播放列表会省略 http://host 前缀,并使用站点根相对路径。"

#: htdocs/luci-static/resources/view/rtp2httpd.js:706
msgid "R2H Token"
msgstr "HTTP 请求认证令牌"
Expand Down
1 change: 1 addition & 0 deletions openwrt-support/rtp2httpd/files/rtp2httpd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ config rtp2httpd
# option hostname 'somehost.example.com'
# option xff '0'
# option app_path_prefix '/app/rtp2httpd'
# option use_relative_path_in_m3u '0'
# option status_page_path '/status'
# option player_page_path '/player'
# option r2h_token 'your-secret-token-here'
Expand Down
4 changes: 4 additions & 0 deletions openwrt-support/rtp2httpd/files/rtp2httpd.init
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ start_instance() {
config_get_bool aux "$cfg" 'zerocopy_on_send' '0'
[ "$aux" = 1 ] && procd_append_param command "--zerocopy-on-send"

# Handle use_relative_path_in_m3u flag
config_get_bool aux "$cfg" 'use_relative_path_in_m3u' '0'
[ "$aux" = 1 ] && procd_append_param command "--use-relative-path-in-m3u"

# Handle xff flag
config_get_bool aux "$cfg" 'xff' '0'
[ "$aux" = 1 ] && procd_append_param command "--xff"
Expand Down
4 changes: 4 additions & 0 deletions rtp2httpd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ verbosity = 1
# without stripping it
;app-path-prefix = /app/rtp2httpd

# Use root-relative paths in generated and rewritten M3U playlists (default: no)
# When enabled, playlist URLs omit the http://host prefix and start with / or app-path-prefix
;use-relative-path-in-m3u = no

# Interface configuration for upstream media streams
# Default interface for all upstream traffic (lowest priority, used when specific interface not set)
;upstream-interface = iptv
Expand Down
19 changes: 18 additions & 1 deletion src/configuration.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ int cmd_fcc_listen_port_range_set = 0;
int cmd_status_page_path_set = 0;
int cmd_player_page_path_set = 0;
int cmd_app_path_prefix_set = 0;
int cmd_use_relative_path_in_m3u_set = 0;
int cmd_zerocopy_on_send_set = 0;
int cmd_workers_set = 0;
int cmd_external_m3u_url_set = 0;
Expand All @@ -58,7 +59,7 @@ int cmd_log_format_set = 0;

enum section_e { SEC_NONE = 0, SEC_BIND, SEC_SERVICES, SEC_GLOBAL };

enum long_option_e { OPT_APP_PATH_PREFIX = 1000, OPT_ACCESS_LOG, OPT_LOG_FORMAT };
enum long_option_e { OPT_APP_PATH_PREFIX = 1000, OPT_USE_RELATIVE_PATH_IN_M3U, OPT_ACCESS_LOG, OPT_LOG_FORMAT };

/* M3U parsing state variables */
static char *inline_m3u_buffer = NULL;
Expand Down Expand Up @@ -611,6 +612,12 @@ void parse_global_sec(char *line) {
return;
}

if (strcasecmp("use-relative-path-in-m3u", param) == 0) {
if (set_if_not_cmd_override(cmd_use_relative_path_in_m3u_set, "use-relative-path-in-m3u"))
config.use_relative_path_in_m3u = parse_bool(value);
return;
}

/* String parameters with command line override */
if (strcasecmp("hostname", param) == 0) {
if (set_if_not_cmd_override(cmd_hostname_set, "hostname")) {
Expand Down Expand Up @@ -1145,6 +1152,8 @@ void config_init(void) {
config.mcast_rejoin_interval = 0;
if (!cmd_zerocopy_on_send_set)
config.zerocopy_on_send = 0;
if (!cmd_use_relative_path_in_m3u_set)
config.use_relative_path_in_m3u = 0;
if (!cmd_fcc_listen_port_range_set) {
config.fcc_listen_port_min = 0;
config.fcc_listen_port_max = 0;
Expand Down Expand Up @@ -1304,6 +1313,8 @@ void usage(FILE *f, char *progname) {
"/player)\n"
"\t --app-path-prefix <path> Public mount path prefix for all HTTP "
"resources (default: none)\n"
"\t --use-relative-path-in-m3u Use root-relative URLs in generated "
"and rewritten M3U playlists (default: off)\n"
"\t-M --external-m3u <url> External M3U playlist URL (file://, http://, "
"https://)\n"
"\t-I --external-m3u-update-interval <seconds> Auto-update interval "
Expand Down Expand Up @@ -1392,6 +1403,7 @@ void parse_cmd_line(int argc, char *argv[]) {
{"status-page-path", required_argument, 0, 's'},
{"player-page-path", required_argument, 0, 'p'},
{"app-path-prefix", required_argument, 0, OPT_APP_PATH_PREFIX},
{"use-relative-path-in-m3u", no_argument, 0, OPT_USE_RELATIVE_PATH_IN_M3U},
{"external-m3u", required_argument, 0, 'M'},
{"external-m3u-update-interval", required_argument, 0, 'I'},
{"zerocopy-on-send", no_argument, 0, 'Z'},
Expand Down Expand Up @@ -1521,6 +1533,11 @@ void parse_cmd_line(int argc, char *argv[]) {
set_app_path_prefix_value(optarg);
cmd_app_path_prefix_set = 1;
break;
case OPT_USE_RELATIVE_PATH_IN_M3U:
config.use_relative_path_in_m3u = 1;
cmd_use_relative_path_in_m3u_set = 1;
logger(LOG_INFO, "Using root-relative URLs in M3U playlists");
break;
case 'i':
strncpy(config.upstream_interface, optarg, IFNAMSIZ - 1);
cmd_upstream_interface_set = 1;
Expand Down
5 changes: 3 additions & 2 deletions src/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ typedef struct {
empty) */

/* Public app mount path settings */
char *app_path_prefix; /* Absolute public app path prefix, or empty string */
char *app_path_route; /* App path prefix without leading slash, or empty string */
char *app_path_prefix; /* Absolute public app path prefix, or empty string */
char *app_path_route; /* App path prefix without leading slash, or empty string */
int use_relative_path_in_m3u; /* Use root-relative URLs in generated/rewritten M3U (0=no, 1=yes) */

/* External M3U settings */
char *external_m3u_url; /* External M3U URL (NULL=none) */
Expand Down
Loading
Loading