From 64ae6e43c584e3e9aa022533d1c4fef1f38d6a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caner=20Ate=C5=9F?= <49502924+cnrats@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:25:25 +0300 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=20fmp4=20H?= =?UTF-8?q?LS=20=E5=88=87=E7=89=87=E6=96=87=E4=BB=B6=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=90=8D=20(.mp4/.m4s)=20=20(#4746)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 背景 / 问题 fmp4 模式下,HLS 切片文件名被硬编码为 `.mp4`(见 `HlsMakerImp::onOpenSegment`),无法改用 fmp4 媒体段更规范、更常见的 `.m4s` 扩展名(`init` 段用 `init.mp4`、媒体段用 `.m4s` 是 CMAF/DASH 的通行约定)。 ## 改动 - 新增配置项 `hls.fmp4SegExt`,默认 `.mp4`,**完全向后兼容**;用户可设为 `.m4s`。 - `init` 段仍固定为 `init.mp4`,mpegts 切片仍为 `.ts`,相关行为不变。 - 切片文件名同时用作 m3u8 中的分片 URI,因此扩展名变更后 playlist 自动保持一致,无需额外同步。 - 在 HTTP MIME 表中注册 `.m4s` -> `video/mp4`;否则会以 `text/plain` 返回,导致 Safari 等播放器无法播放 HLS-fmp4。 - 配置值不带前导点(如 `m4s`)时自动规范化为 `.m4s`。 ## 涉及文件 `src/Common/config.h`、`src/Common/config.cpp`、`src/Record/HlsRecorder.h`、`src/Record/HlsMakerImp.h`、`src/Record/HlsMakerImp.cpp`、`src/Http/HttpConst.cpp`、`conf/config.ini` ## 测试 - **默认(未配置)**:切片仍为 `*.mp4`,`init` 为 `init.mp4`,行为与改动前一致(回归正常)。 - **`hls.fmp4SegExt=.m4s`**:切片文件为 `*_N.m4s`,m3u8 中 `#EXTINF` 的 URI 指向 `.m4s`,`#EXT-X-MAP` 仍为 `init.mp4`;HTTP 拉取切片返回 `Content-Type: video/mp4`;hls.js 与 Safari 播放正常。 - **`hls.fmp4SegExt=m4s`(无前导点)**:自动规范化,仍生成 `*.m4s`。 Co-authored-by: Claude Opus 4.8 (1M context) --- conf/config.ini | 5 +++++ src/Common/config.cpp | 2 ++ src/Common/config.h | 3 +++ src/Http/HttpConst.cpp | 1 + src/Record/HlsMakerImp.cpp | 7 +++++-- src/Record/HlsMakerImp.h | 4 +++- src/Record/HlsRecorder.h | 3 ++- 7 files changed, 21 insertions(+), 4 deletions(-) diff --git a/conf/config.ini b/conf/config.ini index 9be5e411..ec3a557a 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -284,6 +284,11 @@ segKeep=0 # If set to 1, the length of the first segment is forcibly set to exactly 1 GOP. # When the GOP is smaller than `segDur`, this can improve the initial startup (instant playback) speed. fastRegister=0 + +# fmp4 HLS切片文件的扩展名,例如 .mp4 或 .m4s(fmp4媒体段的标准扩展名);init段始终为init.mp4,mpegts切片始终为.ts +# File extension for fMP4 HLS segment files, e.g. .mp4 or .m4s (the standard extension for fMP4 media segments). +# The init segment is always init.mp4, and mpegts segments are always .ts. +fmp4SegExt=.mp4 [hook] # 是否启用hook事件,启用后,推拉流都将进行鉴权 # Whether to enable webhook events. When enabled, pushing and pulling streams requires authentication. diff --git a/src/Common/config.cpp b/src/Common/config.cpp index f1774fe7..591e42df 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -355,6 +355,7 @@ const string kFileBufSize = HLS_FIELD "fileBufSize"; const string kBroadcastRecordTs = HLS_FIELD "broadcastRecordTs"; const string kDeleteDelaySec = HLS_FIELD "deleteDelaySec"; const string kFastRegister = HLS_FIELD "fastRegister"; +const string kFmp4SegExt = HLS_FIELD "fmp4SegExt"; static onceToken token([]() { mINI::Instance()[kSegmentDuration] = 2; @@ -366,6 +367,7 @@ static onceToken token([]() { mINI::Instance()[kBroadcastRecordTs] = false; mINI::Instance()[kDeleteDelaySec] = 10; mINI::Instance()[kFastRegister] = false; + mINI::Instance()[kFmp4SegExt] = ".mp4"; }); } // namespace Hls diff --git a/src/Common/config.h b/src/Common/config.h index d33b71f7..e3f85844 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -531,6 +531,9 @@ extern const std::string kDeleteDelaySec; // 如果设置为1,则第一个切片长度强制设置为1个GOP [AUTO-TRANSLATED:fbbb651d] // If set to 1, the length of the first slice is forced to be 1 GOP extern const std::string kFastRegister; +// fmp4 HLS切片文件的扩展名(例如 .mp4 或 .m4s);mpegts切片始终为.ts +// File extension for fMP4 HLS segment files (e.g. .mp4 or .m4s); mpegts segments are always .ts +extern const std::string kFmp4SegExt; } // namespace Hls // //////////Rtp代理相关配置/////////// [AUTO-TRANSLATED:7b285587] diff --git a/src/Http/HttpConst.cpp b/src/Http/HttpConst.cpp index 7c1a896c..240057d3 100644 --- a/src/Http/HttpConst.cpp +++ b/src/Http/HttpConst.cpp @@ -183,6 +183,7 @@ static const char *s_mime_src[][2] = { {"3gp", "video/3gpp"}, {"ts", "video/mp2t"}, {"mp4", "video/mp4"}, + {"m4s", "video/mp4"}, {"mpeg", "video/mpeg"}, {"mpg", "video/mpeg"}, {"mov", "video/quicktime"}, diff --git a/src/Record/HlsMakerImp.cpp b/src/Record/HlsMakerImp.cpp index 37e49be2..d5c77665 100644 --- a/src/Record/HlsMakerImp.cpp +++ b/src/Record/HlsMakerImp.cpp @@ -31,13 +31,16 @@ std::string getDelayPath(const std::string& originalPath) { } HlsMakerImp::HlsMakerImp(bool is_fmp4, const string &m3u8_file, const string ¶ms, uint32_t bufSize, float seg_duration, - uint32_t seg_number, bool seg_keep) : HlsMaker(is_fmp4, seg_duration, seg_number, seg_keep) { + uint32_t seg_number, bool seg_keep, const string &fmp4_seg_ext) : HlsMaker(is_fmp4, seg_duration, seg_number, seg_keep) { _poller = EventPollerPool::Instance().getPoller(); _path_prefix = m3u8_file.substr(0, m3u8_file.rfind('/')); _path_hls = m3u8_file; _path_hls_delay = getDelayPath(m3u8_file); _params = params; _buf_size = bufSize; + // 兼容用户配置不带前导点的扩展名(例如 m4s),统一补上"." [AUTO-TRANSLATED] + // Tolerate user-configured extensions without a leading dot (e.g. m4s) by normalizing to ".m4s" + _fmp4_seg_ext = fmp4_seg_ext.empty() ? ".mp4" : (fmp4_seg_ext.front() == '.' ? fmp4_seg_ext : "." + fmp4_seg_ext); _file_buf.reset(new char[bufSize], [](char *ptr) { delete[] ptr; }); _info.folder = _path_prefix; } @@ -151,7 +154,7 @@ string HlsMakerImp::onOpenSegment(uint64_t index) { auto strHour = getTimeStr("%H"); auto strTime = getTimeStr("%M-%S"); auto current_dir = strDate + "/" + strHour + "/"; - segment_name = current_dir + strTime + "_" + std::to_string(index) + (isFmp4() ? ".mp4" : ".ts"); + segment_name = current_dir + strTime + "_" + std::to_string(index) + (isFmp4() ? _fmp4_seg_ext : ".ts"); segment_path = _path_prefix + "/" + segment_name; if (isLive()) { // 直播 diff --git a/src/Record/HlsMakerImp.h b/src/Record/HlsMakerImp.h index aa1b9efe..d1af9a07 100644 --- a/src/Record/HlsMakerImp.h +++ b/src/Record/HlsMakerImp.h @@ -22,7 +22,8 @@ namespace mediakit { class HlsMakerImp : public HlsMaker { public: HlsMakerImp(bool is_fmp4, const std::string &m3u8_file, const std::string ¶ms, uint32_t bufSize = 64 * 1024, - float seg_duration = 5, uint32_t seg_number = 3, bool seg_keep = false); + float seg_duration = 5, uint32_t seg_number = 3, bool seg_keep = false, + const std::string &fmp4_seg_ext = ".mp4"); ~HlsMakerImp() override; /** @@ -68,6 +69,7 @@ private: private: int _buf_size; std::string _params; + std::string _fmp4_seg_ext; std::string _path_hls; std::string _path_hls_delay; std::string _path_init; diff --git a/src/Record/HlsRecorder.h b/src/Record/HlsRecorder.h index 6ed4e78f..21992059 100644 --- a/src/Record/HlsRecorder.h +++ b/src/Record/HlsRecorder.h @@ -26,9 +26,10 @@ public: GET_CONFIG(bool, hlsKeep, Hls::kSegmentKeep); GET_CONFIG(uint32_t, hlsBufSize, Hls::kFileBufSize); GET_CONFIG(float, hlsDuration, Hls::kSegmentDuration); + GET_CONFIG(std::string, hlsFmp4SegExt, Hls::kFmp4SegExt); _option = option; - _hls = std::make_shared(is_fmp4, m3u8_file, params, hlsBufSize, hlsDuration, hlsNum, hlsKeep); + _hls = std::make_shared(is_fmp4, m3u8_file, params, hlsBufSize, hlsDuration, hlsNum, hlsKeep, hlsFmp4SegExt); // 清空上次的残余文件 [AUTO-TRANSLATED:e16122be] // Clear the residual files from the last time _hls->clearCache();