PR #25417 opened by xiujunwang-dev
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25417
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25417.patch

Find the output extension in the URL path, not the complete URL. This
prevents dots in the authority or query from truncating generated segment
and subtitle names, and handles URLs with an empty path.

Fixes #11498.

Signed-off-by: Xiujun Wang <[email protected]>



>From a74222f393be14ffab92ca2a073d1f958835bce8 Mon Sep 17 00:00:00 2001
From: Xiujun Wang <[email protected]>
Date: Sat, 24 Oct 2026 21:14:38 +0800
Subject: [PATCH] avformat/hlsenc: preserve dotted hosts in segment names

Find the output extension in the URL path, not the complete URL. This
prevents dots in the authority or query from truncating generated segment
and subtitle names, and handles URLs with an empty path.

Fixes #11498.

Signed-off-by: Xiujun Wang <[email protected]>
---
 libavformat/Makefile            |   1 +
 libavformat/hlsenc.c            |  68 +++++++++++-----
 libavformat/tests/.gitignore    |   1 +
 libavformat/tests/hlsenc.c      | 122 ++++++++++++++++++++++++++++
 tests/fate/libavformat.mak      |   4 +
 tests/ref/fate/hlsenc-filenames | 140 ++++++++++++++++++++++++++++++++
 6 files changed, 314 insertions(+), 22 deletions(-)
 create mode 100644 libavformat/tests/hlsenc.c
 create mode 100644 tests/ref/fate/hlsenc-filenames

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 92c09714fe7..8317304345a 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -799,6 +799,7 @@ FIFO-MUXER-TESTPROGS-$(CONFIG_NETWORK)   += fifo_muxer
 TESTPROGS-$(CONFIG_FIFO_MUXER)           += $(FIFO-MUXER-TESTPROGS-yes)
 TESTPROGS-$(CONFIG_FFRTMPCRYPT_PROTOCOL) += rtmpdh
 TESTPROGS-$(CONFIG_HLS_DEMUXER)          += hls
+TESTPROGS-$(CONFIG_HLS_MUXER)            += hlsenc
 TESTPROGS-$(CONFIG_HTTP_PROTOCOL)        += http
 TESTPROGS-$(CONFIG_NETWORK)              += noproxy
 TESTPROGS-$(CONFIG_SRTP)                 += srtp
diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c
index e35af34d230..86f6e4e4145 100644
--- a/libavformat/hlsenc.c
+++ b/libavformat/hlsenc.c
@@ -1999,6 +1999,41 @@ fail:
     return ret;
 }
 
+static int hls_append_segment_pattern(const char *url, const char *pattern,
+                                      char **filename)
+{
+    URLComponents uc;
+    const char *extension = NULL;
+    const char *path_end;
+    const char *path;
+    const char *p;
+    int add_slash;
+    int ret;
+
+    ret = ff_url_decompose(&uc, url, NULL);
+    if (ret < 0)
+        return ret;
+
+    path = uc.path;
+    path_end = uc.url_component_end_path;
+    add_slash = path == path_end && URL_COMPONENT_HAVE(uc, authority);
+    for (p = path_end; p > path; p--)
+        if (p[-1] == '/' || (HAVE_DOS_PATHS && p[-1] == '\\'))
+            break;
+    path = p;
+
+    for (p = path; p < path_end; p++)
+        if (*p == '.')
+            extension = p;
+
+    if (!extension)
+        extension = path_end;
+
+    *filename = av_asprintf("%.*s%s%s%s", (int)(extension - url), url,
+                            add_slash ? "/" : "", pattern, path_end);
+    return *filename ? 0 : AVERROR(ENOMEM);
+}
+
 static int get_nth_codec_stream_index(AVFormatContext *s,
                                       enum AVMediaType codec_type,
                                       int64_t stream_id)
@@ -3031,16 +3066,9 @@ static int hls_init(AVFormatContext *s)
             if (ret < 0)
                 return ret;
         } else {
-            char *p = strrchr(vs->m3u8_name, '.');
-            if (p)
-                *p = '\0';
-
-            vs->basename = av_asprintf("%s%s", vs->m3u8_name, pattern);
-            if (!vs->basename)
-                return AVERROR(ENOMEM);
-
-            if (p)
-                *p = '.';
+            ret = hls_append_segment_pattern(vs->m3u8_name, pattern, 
&vs->basename);
+            if (ret < 0)
+                return ret;
         }
 
         if (hls->segment_type == SEGMENT_TYPE_FMP4) {
@@ -3104,25 +3132,21 @@ static int hls_init(AVFormatContext *s)
             EXTERN const FFOutputFormat ff_webvtt_muxer;
             vs->vtt_oformat = &ff_webvtt_muxer.p;
 
-            char *p = strrchr(vs->m3u8_name, '.');
-            if (p)
-                *p = '\0';
-
-            vs->vtt_basename = av_asprintf("%s%s", vs->m3u8_name, vtt_pattern);
-            if (!vs->vtt_basename)
-                return AVERROR(ENOMEM);
+            ret = hls_append_segment_pattern(vs->m3u8_name, vtt_pattern,
+                                              &vs->vtt_basename);
+            if (ret < 0)
+                return ret;
 
             if (hls->subtitle_filename) {
                 ret = format_name(hls->subtitle_filename, &vs->vtt_m3u8_name, 
i, vs->varname);
                 if (ret < 0)
                     return ret;
             } else {
-                vs->vtt_m3u8_name = av_asprintf("%s_vtt.m3u8", vs->m3u8_name);
-                if (!vs->vtt_m3u8_name)
-                    return AVERROR(ENOMEM);
+                ret = hls_append_segment_pattern(vs->m3u8_name, "_vtt.m3u8",
+                                                 &vs->vtt_m3u8_name);
+                if (ret < 0)
+                    return ret;
             }
-            if (p)
-                *p = '.';
         }
 
         if ((ret = hls_mux_init(s, vs)) < 0)
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index f3640a487cc..52614a3460d 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -15,3 +15,4 @@
 /http
 /ogg_pages
 /hls
+/hlsenc
diff --git a/libavformat/tests/hlsenc.c b/libavformat/tests/hlsenc.c
new file mode 100644
index 00000000000..544034131f7
--- /dev/null
+++ b/libavformat/tests/hlsenc.c
@@ -0,0 +1,122 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavformat/hlsenc.c"
+
+/* Exercise URL generation without opening network connections or files. */
+static int io_open(AVFormatContext *s, AVIOContext **pb, const char *url,
+                   int flags, AVDictionary **options)
+{
+    return avio_open_dyn_buf(pb);
+}
+
+static int io_close(AVFormatContext *s, AVIOContext *pb)
+{
+    ffio_free_dyn_buf(&pb);
+    return 0;
+}
+
+static int test_filename(const char *url, int single_file, int fmp4)
+{
+    AVFormatContext *s = NULL;
+    AVStream *st;
+    AVDictionary *options = NULL;
+    HLSContext *hls;
+    VariantStream *vs;
+    int ret;
+
+    ret = avformat_alloc_output_context2(&s, NULL, "hls", url);
+    if (ret < 0)
+        return ret;
+
+    s->io_open   = io_open;
+    s->io_close2 = io_close;
+    hls = s->priv_data;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_MPEG2VIDEO;
+    st->codecpar->width      = 16;
+    st->codecpar->height     = 16;
+    st->time_base = (AVRational) { 1, 25 };
+
+    st = avformat_new_stream(s, NULL);
+    if (!st) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
+    st->codecpar->codec_id   = AV_CODEC_ID_WEBVTT;
+    st->time_base = (AVRational) { 1, 1000 };
+
+    if (single_file)
+        av_dict_set(&options, "hls_flags", "single_file", 0);
+    if (fmp4)
+        av_dict_set(&options, "hls_segment_type", "fmp4", 0);
+
+    ret = avformat_write_header(s, &options);
+    if (ret < 0)
+        goto end;
+
+    vs = &hls->var_streams[0];
+    printf("%s (%s, %s)\n", url, fmp4 ? "fmp4" : "mpegts",
+           single_file ? "single file" : "segments");
+    printf("segment: %s\n", vs->avf->url);
+    printf("subtitle: %s\n", vs->vtt_avf->url);
+    printf("subtitle playlist: %s\n", vs->vtt_m3u8_name);
+    printf("playlist: %s\n", vs->m3u8_name);
+    ret = av_write_trailer(s);
+
+end:
+    av_dict_free(&options);
+    avformat_free_context(s);
+    return ret;
+}
+
+int main(void)
+{
+    static const char *const urls[] = {
+        "https://example.ffmpeg";,
+        "https://example.ffmpeg/";,
+        "https://user.name:[email protected]:8443/dir.name/output";,
+        "https://example.ffmpeg/dir.name/output.m3u8";,
+        "https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar";,
+        "dir.name/output",
+        "dir.name/output.m3u8",
+    };
+    int ret;
+
+    av_log_set_level(AV_LOG_ERROR);
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(urls); i++) {
+        for (int single_file = 0; single_file < 2; single_file++) {
+            for (int fmp4 = 0; fmp4 < 2; fmp4++) {
+                ret = test_filename(urls[i], single_file, fmp4);
+                if (ret < 0) {
+                    fprintf(stderr, "%s: %s\n", urls[i], av_err2str(ret));
+                    return 1;
+                }
+            }
+        }
+    }
+    return 0;
+}
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index 18270bbcab9..7bc6109592d 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -115,6 +115,10 @@ fate-hls-parse-master-media-start: CMD = run 
libavformat/tests/hls$(EXESUF) $(TA
 
 FATE_LIBAVFORMAT-$(call ALLYES, HLS_DEMUXER FILE_PROTOCOL) += $(FATE_HLS_PARSE)
 
+FATE_LIBAVFORMAT-$(call ALLYES, HLS_MUXER MP4_MUXER) += fate-hlsenc-filenames
+fate-hlsenc-filenames: libavformat/tests/hlsenc$(EXESUF)
+fate-hlsenc-filenames: CMD = run libavformat/tests/hlsenc$(EXESUF)
+
 FATE_LIBAVFORMAT-$(CONFIG_IMF_DEMUXER) += fate-imf
 fate-imf: libavformat/tests/imf$(EXESUF)
 fate-imf: CMD = run libavformat/tests/imf$(EXESUF)
diff --git a/tests/ref/fate/hlsenc-filenames b/tests/ref/fate/hlsenc-filenames
new file mode 100644
index 00000000000..aeabc4fa87c
--- /dev/null
+++ b/tests/ref/fate/hlsenc-filenames
@@ -0,0 +1,140 @@
+https://example.ffmpeg (mpegts, segments)
+segment: https://example.ffmpeg/0.ts
+subtitle: https://example.ffmpeg/0.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg
+https://example.ffmpeg (fmp4, segments)
+segment: https://example.ffmpeg/0.m4s
+subtitle: https://example.ffmpeg/0.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg
+https://example.ffmpeg (mpegts, single file)
+segment: https://example.ffmpeg/.ts
+subtitle: https://example.ffmpeg/.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg
+https://example.ffmpeg (fmp4, single file)
+segment: https://example.ffmpeg/.m4s
+subtitle: https://example.ffmpeg/.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg
+https://example.ffmpeg/ (mpegts, segments)
+segment: https://example.ffmpeg/0.ts
+subtitle: https://example.ffmpeg/0.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg/
+https://example.ffmpeg/ (fmp4, segments)
+segment: https://example.ffmpeg/0.m4s
+subtitle: https://example.ffmpeg/0.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg/
+https://example.ffmpeg/ (mpegts, single file)
+segment: https://example.ffmpeg/.ts
+subtitle: https://example.ffmpeg/.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg/
+https://example.ffmpeg/ (fmp4, single file)
+segment: https://example.ffmpeg/.m4s
+subtitle: https://example.ffmpeg/.vtt
+subtitle playlist: https://example.ffmpeg/_vtt.m3u8
+playlist: https://example.ffmpeg/
+https://user.name:[email protected]:8443/dir.name/output (mpegts, 
segments)
+segment: https://user.name:[email protected]:8443/dir.name/output0.ts
+subtitle: https://user.name:[email protected]:8443/dir.name/output0.vtt
+subtitle playlist: 
https://user.name:[email protected]:8443/dir.name/output_vtt.m3u8
+playlist: https://user.name:[email protected]:8443/dir.name/output
+https://user.name:[email protected]:8443/dir.name/output (fmp4, segments)
+segment: https://user.name:[email protected]:8443/dir.name/output0.m4s
+subtitle: https://user.name:[email protected]:8443/dir.name/output0.vtt
+subtitle playlist: 
https://user.name:[email protected]:8443/dir.name/output_vtt.m3u8
+playlist: https://user.name:[email protected]:8443/dir.name/output
+https://user.name:[email protected]:8443/dir.name/output (mpegts, single 
file)
+segment: https://user.name:[email protected]:8443/dir.name/output.ts
+subtitle: https://user.name:[email protected]:8443/dir.name/output.vtt
+subtitle playlist: 
https://user.name:[email protected]:8443/dir.name/output_vtt.m3u8
+playlist: https://user.name:[email protected]:8443/dir.name/output
+https://user.name:[email protected]:8443/dir.name/output (fmp4, single 
file)
+segment: https://user.name:[email protected]:8443/dir.name/output.m4s
+subtitle: https://user.name:[email protected]:8443/dir.name/output.vtt
+subtitle playlist: 
https://user.name:[email protected]:8443/dir.name/output_vtt.m3u8
+playlist: https://user.name:[email protected]:8443/dir.name/output
+https://example.ffmpeg/dir.name/output.m3u8 (mpegts, segments)
+segment: https://example.ffmpeg/dir.name/output0.ts
+subtitle: https://example.ffmpeg/dir.name/output0.vtt
+subtitle playlist: https://example.ffmpeg/dir.name/output_vtt.m3u8
+playlist: https://example.ffmpeg/dir.name/output.m3u8
+https://example.ffmpeg/dir.name/output.m3u8 (fmp4, segments)
+segment: https://example.ffmpeg/dir.name/output0.m4s
+subtitle: https://example.ffmpeg/dir.name/output0.vtt
+subtitle playlist: https://example.ffmpeg/dir.name/output_vtt.m3u8
+playlist: https://example.ffmpeg/dir.name/output.m3u8
+https://example.ffmpeg/dir.name/output.m3u8 (mpegts, single file)
+segment: https://example.ffmpeg/dir.name/output.ts
+subtitle: https://example.ffmpeg/dir.name/output.vtt
+subtitle playlist: https://example.ffmpeg/dir.name/output_vtt.m3u8
+playlist: https://example.ffmpeg/dir.name/output.m3u8
+https://example.ffmpeg/dir.name/output.m3u8 (fmp4, single file)
+segment: https://example.ffmpeg/dir.name/output.m4s
+subtitle: https://example.ffmpeg/dir.name/output.vtt
+subtitle playlist: https://example.ffmpeg/dir.name/output_vtt.m3u8
+playlist: https://example.ffmpeg/dir.name/output.m3u8
+https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar (mpegts, segments)
+segment: https://example.ffmpeg/dir.name/output0.ts?token=foo.bar
+subtitle: https://example.ffmpeg/dir.name/output0.vtt?token=foo.bar
+subtitle playlist: 
https://example.ffmpeg/dir.name/output_vtt.m3u8?token=foo.bar
+playlist: https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar
+https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar (fmp4, segments)
+segment: https://example.ffmpeg/dir.name/output0.m4s?token=foo.bar
+subtitle: https://example.ffmpeg/dir.name/output0.vtt?token=foo.bar
+subtitle playlist: 
https://example.ffmpeg/dir.name/output_vtt.m3u8?token=foo.bar
+playlist: https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar
+https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar (mpegts, single file)
+segment: https://example.ffmpeg/dir.name/output.ts?token=foo.bar
+subtitle: https://example.ffmpeg/dir.name/output.vtt?token=foo.bar
+subtitle playlist: 
https://example.ffmpeg/dir.name/output_vtt.m3u8?token=foo.bar
+playlist: https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar
+https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar (fmp4, single file)
+segment: https://example.ffmpeg/dir.name/output.m4s?token=foo.bar
+subtitle: https://example.ffmpeg/dir.name/output.vtt?token=foo.bar
+subtitle playlist: 
https://example.ffmpeg/dir.name/output_vtt.m3u8?token=foo.bar
+playlist: https://example.ffmpeg/dir.name/output.m3u8?token=foo.bar
+dir.name/output (mpegts, segments)
+segment: dir.name/output0.ts
+subtitle: dir.name/output0.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output
+dir.name/output (fmp4, segments)
+segment: dir.name/output0.m4s
+subtitle: dir.name/output0.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output
+dir.name/output (mpegts, single file)
+segment: dir.name/output.ts
+subtitle: dir.name/output.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output
+dir.name/output (fmp4, single file)
+segment: dir.name/output.m4s
+subtitle: dir.name/output.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output
+dir.name/output.m3u8 (mpegts, segments)
+segment: dir.name/output0.ts
+subtitle: dir.name/output0.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output.m3u8
+dir.name/output.m3u8 (fmp4, segments)
+segment: dir.name/output0.m4s
+subtitle: dir.name/output0.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output.m3u8
+dir.name/output.m3u8 (mpegts, single file)
+segment: dir.name/output.ts
+subtitle: dir.name/output.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output.m3u8
+dir.name/output.m3u8 (fmp4, single file)
+segment: dir.name/output.m4s
+subtitle: dir.name/output.vtt
+subtitle playlist: dir.name/output_vtt.m3u8
+playlist: dir.name/output.m3u8
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to