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]
