☰
DASH SegmentTemplate 实战:从变量选择到直播点播配置避坑指南
2026/10/20 16:18:08 网站建设 项目流程

1. 为什么 segmentTemplate 值得单独拎出来讲

做流媒体自适应播放这一块,MPD 文件里的SegmentTemplate是我见过最容易让人“一看就懂、一写就错”的元素。它不像SegmentList那样把每个分片地址老老实实列出来,也不像SegmentBase那样只给一个索引范围就完事,而是用一套模板语法配合几个变量,把成百上千个分片地址“算”出来。写对了,MPD 文件可以精简到几十行;写错了,播放器要么找不到分片,要么时间轴对不上,排查起来非常折磨人。

这篇文章面向的是正在做 DASH 打包、转码或者播放器适配的开发者,尤其是那些已经能跑通一个基础 MPD、但一碰到直播、多码率、多语言轨道就卡住的人。我会把SegmentTemplate的核心机制、变量含义、常见组合方式、实操中踩过的坑,以及一套可以直接抄的配置模板讲清楚。读完你应该能做到:拿到一个SegmentTemplate的 MPD,能快速判断它的分片寻址逻辑;自己写打包脚本时,知道该用$Number$还是$Time$,该不该配SegmentTimeline,startNumber和duration怎么定。

先给一个最直观的类比。SegmentTemplate就像快递面单上的地址模板:$RepresentationID$是收件人编号,$Number$是第几个包裹,$Time$是发货时间戳,$Bandwidth$是包裹重量等级。你只要把模板和变量填好,系统就能自动生成每一件的完整地址。问题在于,这个“自动生成”的规则在不同场景下差别很大,点播和直播、恒定帧率和可变帧率、单轨和多轨,用的变量和配套元素都不一样。

我见过不少团队在点播场景用$Number$跑得好好的,一上直播就出问题,原因就是没搞清楚$Number$和$Time$的适用边界。也见过有人为了省事,所有场景都硬塞SegmentTimeline,结果 MPD 体积暴涨,播放器解析变慢。这些坑后面会逐个拆开讲。

2. SegmentTemplate 的核心机制与变量体系

2.1 模板语法到底是怎么拼出分片地址的

SegmentTemplate的本质是一个字符串替换引擎。MPD 里写一个带占位符的 URL 模板,播放器在请求某个分片时,把占位符替换成实际值,拼出最终地址。这个替换过程发生在播放器内部,不需要服务端做任何动态计算,所以它非常适合 CDN 缓存——因为每个分片的地址是确定性的,只要模板和变量规则一致,任何播放器算出来的地址都一样。

一个典型的模板长这样:

<SegmentTemplate media="video/$RepresentationID$/seg-$Number$.m4s" initialization="video/$RepresentationID$/init.mp4" startNumber="1" duration="4" timescale="90000" />

这里media是分片地址模板,initialization是初始化分片地址模板。播放器拿到这个之后,对于RepresentationID为v1的轨道,第 1 个分片就是video/v1/seg-1.m4s,第 2 个是video/v1/seg-2.m4s,以此类推。duration="4"配合timescale="90000"表示每个分片时长是 4 秒(因为 4 × 90000 / 90000 = 4 秒,这里 duration 的单位是 timescale 的倒数,所以 duration 直接写 4 就代表 4 秒,前提是 timescale 和 duration 的比值关系正确)。

这里有个容易混淆的点:duration的单位不是秒,而是 timescale 的刻度数。如果timescale="90000",duration="360000",那每个分片就是 4 秒。很多人第一次写的时候直接把duration写成 4,结果播放器算出每个分片只有 4/90000 秒,整个时间轴全乱。我建议在模板里始终显式写出timescale,并且用“秒数 × timescale”的方式算duration,这样不容易错。

2.2 $Number$ 与 $Time$ 的分工和选择逻辑

$Number$和$Time$是SegmentTemplate里最核心的两个变量,但它们解决的问题不一样。

$Number$表示分片的序号,从startNumber开始递增。它的前提是每个分片时长固定,播放器可以用“第几个分片 × 分片时长”推算出这个分片对应的时间段。点播场景下,如果视频是恒定帧率、每个分片时长一致,用$Number$是最简洁的。比如一个 10 分钟的视频,每个分片 4 秒,总共 150 个分片,startNumber="1",播放器从 1 数到 150 就行。

$Time$表示分片起始时间戳,单位是 timescale 的刻度。它不依赖分片序号,而是直接告诉你这个分片从哪个时间点开始。直播场景下,分片是持续生成的,序号可能会因为服务重启、切片策略调整而重置,但时间戳是连续的。这时候用$Time$更稳,因为播放器不需要知道“这是第几个分片”,只需要知道“这个分片从哪个时间开始”。

选择逻辑可以总结成一句话:分片时长固定且序号连续,用$Number$;分片时长可能变化或序号可能不连续,用$Time$。直播场景我几乎总是推荐$Time$,因为直播的切片边界往往受关键帧位置影响,不一定严格等长。点播场景如果转码参数固定,$Number$更简单。

但$Time$单独用有个问题:播放器怎么知道每个分片有多长?它需要配合SegmentTimeline来告诉播放器每个分片的起始时间和持续时间。SegmentTimeline里用S元素列出每个分片的t(起始时间)和d(持续时间),播放器按这个时间轴去请求。如果不用SegmentTimeline,只写$Time$和duration,那播放器会假设每个分片时长固定,按duration去推算,这在直播里往往不成立。

2.3 SegmentTimeline 的引入时机与代价

SegmentTimeline是SegmentTemplate的“时间轴说明书”。它长这样:

<SegmentTemplate media="live/$RepresentationID$/$Time$.m4s" initialization="live/$RepresentationID$/init.mp4" timescale="90000"> <SegmentTimeline> <S t="0" d="360000" r="9" /> <S t="3600000" d="270000" /> <S t="3870000" d="360000" r="4" /> </SegmentTimeline> </SegmentTemplate>

这里S元素的t是起始时间,d是持续时间,r是重复次数。r="9"表示从t="0"开始,连续 10 个分片(1 个初始 + 9 个重复),每个时长d="360000"。然后下一个S从t="3600000"开始,时长变成270000,说明这里有一个不等长的分片。再下一个S又恢复360000并重复 4 次。

SegmentTimeline的代价是 MPD 体积会变大。直播场景下,如果每个分片都单独列一个S,一个几小时的直播 MPD 可能几 MB,播放器每次刷新都要重新解析,开销不小。所以实践中会用r来压缩连续等长的分片。但r只能压缩“连续且等长”的分片,一旦遇到不等长就得新开一个S。直播切片如果关键帧间隔不稳定,S的数量就会很多。

我的经验是:直播场景如果切片严格按固定时长(比如强制每 2 秒一个关键帧),可以不用SegmentTimeline,直接用$Number$+duration;如果切片受关键帧影响、时长有波动,就必须用SegmentTimeline+$Time$。点播场景如果转码时已经保证了分片等长,$Number$足够;如果源本身是可变帧率或者分片不等长,也得用SegmentTimeline。

3. 多码率与多轨道场景下的模板设计

3.1 RepresentationID 与 Bandwidth 的配合方式

多码率是 DASH 的核心能力,SegmentTemplate要能为每个码率轨道生成独立的分片地址。最常用的变量是$RepresentationID$和$Bandwidth$。

$RepresentationID$是你在Representation元素里给每条轨道起的 ID,比如v1、v2、a1。它适合用来做目录区分:

<SegmentTemplate media="$RepresentationID$/seg-$Number$.m4s" initialization="$RepresentationID$/init.mp4" startNumber="1" duration="4" timescale="90000" />

这样v1轨道的分片在v1/目录下,v2在v2/目录下,互不干扰。$Bandwidth$是轨道的带宽值,比如1500000、3000000。它适合用来做文件名区分:

<SegmentTemplate media="video/$Bandwidth$/seg-$Number$.m4s" initialization="video/$Bandwidth$/init.mp4" ... />

用$Bandwidth$的好处是文件名直接体现码率,排查问题时一眼能看出是哪个轨道。坏处是如果码率值调整了,文件名会变,缓存可能失效。用$RepresentationID$更稳定,因为 ID 是你自己定的,不会因为码率微调而变。我通常推荐$RepresentationID$,然后在 MPD 的Representation里把bandwidth属性写清楚,播放器选轨时看的是bandwidth,不是文件名。

3.2 音视频分离与多语言轨道的模板组织

音视频分离是现代流媒体打包的标准做法,视频轨和音频轨各自有独立的AdaptationSet,各自有SegmentTemplate。音频轨如果有多种语言,每个语言一个AdaptationSet,里面的Representation用不同的lang属性区分。

模板设计上,我习惯用这样的目录结构:

stream/ video/ v1/ init.mp4 seg-1.m4s seg-2.m4s v2/ init.mp4 seg-1.m4s audio/ en/ init.mp4 seg-1.m4s zh/ init.mp4 seg-1.m4s

对应的SegmentTemplate分别是:

<!-- 视频轨 --> <SegmentTemplate media="video/$RepresentationID$/seg-$Number$.m4s" initialization="video/$RepresentationID$/init.mp4" startNumber="1" duration="4" timescale="90000" /> <!-- 音频轨 --> <SegmentTemplate media="audio/$RepresentationID$/seg-$Number$.m4s" initialization="audio/$RepresentationID$/init.mp4" startNumber="1" duration="4" timescale="48000" />

注意音频的timescale通常和视频不一样,音频常用 48000(采样率),视频常用 90000。duration要根据各自的timescale算,不能直接照搬。比如音频每个分片 4 秒,timescale="48000",那duration="192000"。如果音频分片和视频分片要对齐,duration算出来的秒数必须一致,否则播放器在切换轨道时会出现音视频不同步。

3.3 startNumber 的取值陷阱与对齐策略

startNumber看起来简单,但它在多轨道场景下有个隐藏陷阱:如果不同轨道的startNumber不一致,播放器在切换轨道时可能会算错分片序号。

假设视频轨startNumber="1",音频轨startNumber="0",播放器从视频轨切到音频轨时,如果它按“当前时间 / 分片时长”算出应该请求第 5 个分片,视频轨请求的是seg-5.m4s,音频轨请求的是seg-4.m4s,因为音频的序号从 0 开始。这就导致音视频错位。

我的做法是:所有轨道的startNumber统一从 1 开始,并且所有轨道的分片时长在秒级上对齐。如果音频分片和视频分片时长不同(比如视频 4 秒、音频 2 秒),那startNumber也要保证在时间轴上对齐。更稳妥的方式是用$Time$+SegmentTimeline,让播放器直接按时间戳请求,不依赖序号。

还有一个坑:直播场景下,如果服务重启,startNumber可能会重置。比如重启前最后一个分片是seg-1000.m4s,重启后如果startNumber又回到 1,播放器会以为分片序号倒退了,可能触发重新加载或者报错。用$Time$可以避免这个问题,因为时间戳是单调递增的,不会因为重启而回退。

4. 点播与直播场景的完整配置实操

4.1 点播场景:恒定分片时长的最简配置

点播场景如果转码时已经保证了每个分片等长,配置可以非常简洁。下面是一个 1080p 和 720p 双码率、音视频分离的点播 MPD 片段:

<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static" mediaPresentationDuration="PT600S" minBufferTime="PT2S"> <Period> <AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1"> <Representation id="v1" bandwidth="3000000" width="1920" height="1080"> <SegmentTemplate media="video/v1/seg-$Number$.m4s" initialization="video/v1/init.mp4" startNumber="1" duration="360000" timescale="90000" /> </Representation> <Representation id="v2" bandwidth="1500000" width="1280" height="720"> <SegmentTemplate media="video/v2/seg-$Number$.m4s" initialization="video/v2/init.mp4" startNumber="1" duration="360000" timescale="90000" /> </Representation> </AdaptationSet> <AdaptationSet mimeType="audio/mp4" lang="zh" segmentAlignment="true"> <Representation id="a1" bandwidth="128000"> <SegmentTemplate media="audio/a1/seg-$Number$.m4s" initialization="audio/a1/init.mp4" startNumber="1" duration="192000" timescale="48000" /> </Representation> </AdaptationSet> </Period> </MPD>

这里视频duration="360000"、timescale="90000",算出来每个分片 4 秒。音频duration="192000"、timescale="48000",也是 4 秒。两者在秒级上对齐,播放器切换轨道时不会错位。mediaPresentationDuration="PT600S"表示整个视频 10 分钟,播放器可以用这个值校验分片总数:600 / 4 = 150 个分片。

实操中我建议在打包脚本里加一个校验:算出每个轨道的分片总数,确认视频轨和音频轨的总时长一致。如果视频轨 150 个分片、音频轨 149 个,说明最后一个音频分片可能被截断了,播放到结尾时会出现音频提前结束的问题。

4.2 直播场景:SegmentTimeline 加 $Time$ 的稳定写法

直播场景我推荐用$Time$+SegmentTimeline,下面是一个直播 MPD 的片段:

<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="dynamic" minimumUpdatePeriod="PT4S" timeShiftBufferDepth="PT60S" availabilityStartTime="2024-01-01T00:00:00Z"> <Period start="PT0S"> <AdaptationSet mimeType="video/mp4" segmentAlignment="true"> <Representation id="v1" bandwidth="3000000" width="1920" height="1080"> <SegmentTemplate media="live/v1/$Time$.m4s" initialization="live/v1/init.mp4" timescale="90000"> <SegmentTimeline> <S t="0" d="180000" r="29" /> <S t="5400000" d="135000" /> <S t="5535000" d="180000" r="14" /> </SegmentTimeline> </SegmentTemplate> </Representation> </AdaptationSet> </Period> </MPD>

这里type="dynamic"表示直播,minimumUpdatePeriod="PT4S"表示播放器每 4 秒刷新一次 MPD,timeShiftBufferDepth="PT60S"表示时移窗口 60 秒。SegmentTimeline里第一个S从t="0"开始,d="180000"(2 秒),重复 29 次,覆盖 60 秒。然后有一个不等长的分片d="135000"(1.5 秒),再后面又恢复 2 秒并重复 14 次。

直播场景的关键是minimumUpdatePeriod和timeShiftBufferDepth的配合。minimumUpdatePeriod太短,播放器频繁刷新 MPD,服务端压力大;太长,播放器可能错过新分片。我一般设成和分片时长一致,比如分片 2 秒,minimumUpdatePeriod就设 2 秒或 4 秒。timeShiftBufferDepth决定观众能回看多久,设 60 秒就是能回看 1 分钟,设 300 秒就是 5 分钟。这个值越大,服务端要保留的分片越多,存储成本越高。

4.3 参数计算:duration、timescale 与分片时长的换算

duration、timescale和分片时长的关系是:

分片时长(秒) = duration / timescale

反过来:

duration = 分片时长(秒) × timescale

常见组合:

场景timescale目标分片时长duration 取值
视频 90kHz900002 秒180000
视频 90kHz900004 秒360000
视频 90kHz900006 秒540000
音频 48kHz480002 秒96000
音频 48kHz480004 秒192000
音频 48kHz480006 秒288000

这里有个细节:视频的timescale不一定是 90000,有些打包工具用 1000 或者帧率作为timescale。如果timescale="1000",那duration="2000"才是 2 秒。我建议统一用 90000,因为它是 MPEG 体系里的标准值,兼容性最好。

音频的timescale通常用采样率,比如 48000 或 44100。如果音频是 AAC 编码,timescale用采样率可以让每个 AAC 帧的时间戳精确对齐。但分片时长不一定是整数秒,比如 2 秒的 AAC 分片,在 48000 采样率下是 96000 个采样点,duration="96000"正好。如果采样率是 44100,2 秒是 88200,duration="88200"也是整数。但如果分片时长是 2.5 秒,44100 × 2.5 = 110250,也是整数。所以音频用采样率做timescale一般不会出现小数问题。

5. 常见问题与排查技巧实录

5.1 播放器报 404:分片地址拼错的几种典型情况

404 是SegmentTemplate最常见的报错,原因通常是模板拼出来的地址和实际文件路径不一致。我整理了几种典型情况:

现象可能原因排查方法
所有分片都 404media模板路径写错,比如少了目录层级手动用模板拼一个地址,和实际文件路径对比
第一个分片正常,后续 404startNumber和实际文件序号不一致检查实际文件是从 0 还是 1 开始命名
某些码率 404$RepresentationID$和实际目录名不匹配检查Representation的id属性和目录名是否一致
直播分片 404$Time$拼出的时间戳和实际文件名不一致检查timescale和实际文件命名时用的时间单位是否一致

我踩过的一个坑是:打包工具生成的文件名用的是seg-00001.m4s(带前导零),但模板写的是seg-$Number$.m4s,拼出来是seg-1.m4s,对不上。解决办法是要么模板里用%05d格式化(但 DASH 标准不支持这种格式化),要么打包时就不带前导零。我后来统一改成不带前导零,模板和文件名都简单。

5.2 时间轴错乱:duration 与 timescale 不匹配的排查

时间轴错乱的典型表现是:播放器能加载分片,但播放进度条乱跳,或者音视频不同步。原因通常是duration和timescale的比值算错了。

排查步骤:

  1. 确认timescale的值。视频通常是 90000,音频通常是采样率。
  2. 用duration / timescale算出每个分片的秒数。
  3. 用mediaPresentationDuration(点播)或实际直播时长除以分片秒数,算出预期分片总数。
  4. 对比实际文件数量,如果对不上,说明duration或timescale有问题。

我遇到过一个案例:视频timescale="90000",duration="4",打包的人以为duration="4"就是 4 秒,结果每个分片只有 4/90000 秒,播放器算出几千个分片,实际文件只有几百个,时间轴完全错乱。改成duration="360000"后正常。

5.3 直播场景下 SegmentTimeline 不更新的处理

直播场景下,SegmentTimeline需要随着新分片的产生不断更新。如果播放器刷新 MPD 后发现SegmentTimeline没变,就会一直请求旧分片,导致播放卡住。

常见原因和解决办法:

  • 服务端没有更新 MPD:检查 MPD 生成逻辑,确认每次新分片产生后都重新写入了 MPD 文件。
  • CDN 缓存了旧 MPD:MPD 文件要设置较短的缓存时间,比如Cache-Control: max-age=2,或者用查询参数区分版本。
  • minimumUpdatePeriod设得太长:如果设成 30 秒,播放器 30 秒才刷新一次,中间的新分片就错过了。建议设成和分片时长一致。
  • SegmentTimeline的r值没有更新:如果新分片是连续等长的,服务端可能只更新了r值,但播放器如果缓存了旧 MPD,就看不到新的r。确保 MPD 的publishTime或availabilityStartTime有变化,让播放器知道要重新加载。

5.4 多码率切换时的音视频不同步问题

多码率切换时音视频不同步,通常是因为视频轨和音频轨的分片边界没有对齐。比如视频分片从 0 秒开始,音频分片从 0.5 秒开始,播放器切轨时就会错位。

解决办法:

  • 打包时强制视频和音频的分片边界对齐,比如都从关键帧位置切。
  • 用$Time$+SegmentTimeline,让播放器按时间戳请求,不依赖序号。
  • 在AdaptationSet里设置segmentAlignment="true",告诉播放器各轨道的分片是对齐的。

我实测下来,最稳的方式是打包时用同一个时间基(比如都用 90000),并且强制每个分片从关键帧开始。这样视频和音频的分片边界天然对齐,切换时不会错位。

6. 打包脚本中的模板生成与校验思路

6.1 用脚本自动生成 SegmentTemplate 配置

手动写 MPD 容易出错,我通常用脚本生成。下面是一个 Python 片段的思路:

def build_segment_template(rep_id, media_dir, init_file, start_number, duration, timescale): return f'''<SegmentTemplate media="{media_dir}/{rep_id}/seg-$Number$.m4s" initialization="{media_dir}/{rep_id}/{init_file}" startNumber="{start_number}" duration="{duration}" timescale="{timescale}" />'''

这个函数接收轨道 ID、媒体目录、初始化文件名、起始序号、duration 和 timescale,返回SegmentTemplate的 XML 字符串。实际使用时,从转码配置里读取每个轨道的参数,循环生成即可。

关键是要保证duration和timescale的比值在所有轨道上一致。我通常会在脚本里加一个断言:

assert duration / timescale == expected_segment_duration

如果算出来不是预期的分片时长,脚本直接报错,避免生成错误的 MPD。

6.2 生成后的自检清单

MPD 生成后,我习惯做几项自检:

  1. 分片总数校验:用mediaPresentationDuration / 分片时长算出预期分片数,和实际文件数对比。
  2. 音视频对齐校验:确认视频轨和音频轨的分片时长在秒级上一致。
  3. 地址拼接校验:手动用模板拼几个分片地址,确认文件存在。
  4. 时间轴连续性校验:如果是SegmentTimeline,确认每个S的t和上一个S的t + d × (r + 1)衔接得上,没有空隙或重叠。

这几项检查能覆盖大部分常见错误。我见过一个案例,SegmentTimeline里两个S之间差了 1 个 timescale 刻度,导致播放器在切换时卡了一帧。这种细微的误差肉眼很难发现,但用脚本校验就能查出来。

6.3 与播放器兼容性的实测经验

不同播放器对SegmentTemplate的支持程度有差异。我实测下来:

  • 主流浏览器原生 DASH 播放:对$Number$+duration支持最好,SegmentTimeline也支持,但r值太大时解析可能变慢。
  • 开源播放器库:对$Time$+SegmentTimeline支持更完整,直播场景推荐用这套。
  • 部分老旧设备:可能不支持$Bandwidth$变量,建议用$RepresentationID$。

我一般会准备两套 MPD:一套用$Number$+duration给点播和兼容性要求高的场景,一套用$Time$+SegmentTimeline给直播和需要精确时间轴的场景。根据实际播放环境切换。

7. 几个我踩过的坑和对应的解法

第一个坑是startNumber默认值。DASH 标准里startNumber的默认值是 1,但有些打包工具生成的文件是从 0 开始命名的。如果你不显式写startNumber="0",播放器会从 1 开始请求,第一个分片就 404。我的做法是永远显式写startNumber,不依赖默认值。

第二个坑是duration和SegmentTimeline同时存在。有些打包工具会同时写duration和SegmentTimeline,播放器可能优先用SegmentTimeline,也可能优先用duration,行为不一致。我的做法是二选一:用SegmentTimeline就不写duration,用duration就不写SegmentTimeline。

第三个坑是直播场景下availabilityStartTime的设置。这个值决定了直播的时间原点,如果设错了,播放器算出来的可用分片范围会不对。我通常设成直播开始的时间,并且确保SegmentTimeline里的t是相对于这个时间原点的偏移。

第四个坑是timescale不一致导致的音视频不同步。视频用 90000,音频用 48000,如果duration算错了,音视频分片时长就不一致。我的做法是在打包脚本里统一用秒数计算,然后分别乘以各自的timescale,确保秒级对齐。

第五个坑是 MPD 的type属性。点播必须写type="static",直播必须写type="dynamic"。如果写错了,播放器可能按错误的模式解析,导致分片请求逻辑不对。我见过有人把直播 MPD 写成static,播放器只加载一次就不刷新了,直播画面卡住不动。

这些坑看起来都是小细节,但每一个都可能导致播放失败。我的建议是:写完 MPD 后,不要急着上播放器测试,先用脚本做一轮自检,把地址拼接、时间轴连续性、音视频对齐这几项查一遍,能省掉很多调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询