流媒体系统把连续采集的音视频编码、传输并按时间播放。协议选择先区分两段链路:采集端到媒体服务的 ingest/contribution,以及媒体服务到观众的 distribution。前者可能用 SRT,后者用 HLS;不能把它们当作只能选一个的竞争方案。
| 场景 | 主要约束 | 对应关注点 |
| 点播 | 起播、搜索跳转、画质与成本 | 媒体分段、缓存、ABR |
| 单向直播 | 距离现场多远、能否稳定播放 | 发布速度、live edge、缓冲 |
| 双向互动 | 对话与操作反馈是否及时 | 双向路径、拥塞控制、播放期限 |
| 跨公网回传 | 画面完整性与丢包恢复 | 重传窗口、带宽余量、接收延迟 |
这些是测量维度,不是固定延迟排名。相同协议在不同编码器、缓冲与网络条件下,可以得到完全不同的端到端时延。
三种时间:起播、直播延迟与缓冲
起播时间从用户发起播放到第一帧显示;玻璃到玻璃延迟从真实场景采集到观众显示;缓冲水位是播放器已经拿到但尚未播放的可用媒体时长。点播可以在很短时间起播并保留较深缓冲,不能把“缓冲十秒”写成“点播晚了十秒”。
直播延迟可能包含采集编码、回传、分段发布等待、清单发现、下载、播放器安全距离与显示。若某一阶段按段等待,段长会影响它,但总延迟不恒等于 segment 时长 × 缓冲段数。计算时还要避免把同一份 hold-back 与缓冲重复计入。
例如明确划分后的阶段分别为 0.1、0.2、0.5、0.1、0.2、1.0、0.1 秒,总计 2.2 秒。即使把发布等待缩短到 0.1 秒,其他阶段不变也仍有 1.8 秒;不能只看到 100 ms part 就宣称系统实现 100 ms 延迟。
ABR:从下载时间推演缓冲变化
服务端提供不同码率和分辨率的表示,客户端依据吞吐、缓冲、设备解码能力等选择。为了顺利切换,媒体时间线、随机访问点和编码约束需要配合;“段编号相同”本身不能保证无缝切档。
假设每段长 2 秒,编码码率 4 Mbit/s,则该段约 8 Mbit;链路实际可用吞吐 2 Mbit/s,下载需要 4 秒。这四秒里播放器消耗四秒旧内容,仅补回两秒新内容,净亏两秒缓冲。
from fractions import Fraction as F
def next_buffer(buffer, duration, bitrate, throughput):
download = duration * bitrate / throughput
stall = max(F(0), download - buffer)
return max(F(0), buffer - download) + duration, stall
buffer = F(6)
trace = []
for _ in range(3):
buffer, stall = next_buffer(buffer, F(2), F(4), F(2))
trace.append((buffer, stall))
assert trace == [(4, 0), (2, 0), (2, 2)]
assert next_buffer(F(6), F(2), F(2), F(2)) == (6, 0)
ladder = [F('0.8'), F('1.4'), F('2.8'), F(5)]
assert max(rate for rate in ladder if rate <= F(2) * F('0.8')) == F('1.4')
print('逐段缓冲亏空、卡顿与保守选档模型通过')
这是恒定段码率、无请求开销的教学模型。真实 VBR 段大小会变化,吞吐估算也会受采样窗口、缓存命中、连接启动以及低延迟内容尚未产出等因素影响。不能断言 on-off 下载永远高估或永远低估带宽。dash.js 吞吐计算
基于吞吐与基于缓冲的规则可结合,实际播放器还会考虑放弃过慢请求、掉帧和频繁切换。画面卡顿也可能来自源站缺段、时间戳不连续或终端解码不足,并非几乎都由 ABR 算法造成。dash.js ABR
下载一段补两秒,为什么缓冲反而减少?
默认条件连续下载三段,缓冲从6到4到2,第三次出现2秒卡顿。途中降码率会改变未来下载耗时,但不会改写已经发生的卡顿。这样才能把一次算式变成可追踪的累积过程。
D=cTr,stall=max(0,D−B),Bnext=max(0,B−D)+T
正在呈现知识画面
下载一段补两秒,为什么缓冲反而减少?下载期间旧内容持续播放。下载耗时超过剩余缓冲就卡顿,新段完成后才补回其媒体时长。
{"id":"abr-buffer-evolution","title":"下载一段补两秒,为什么缓冲反而减少?","summary":"下载期间旧内容持续播放。下载耗时超过剩余缓冲就卡顿,新段完成后才补回其媒体时长。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"bitrate\" data-i18n=\"bitrate\"></label><output for=\"bitrate\" id=\"bitrate-value\"></output><input id=\"bitrate\" type=\"range\" min=\"1\" max=\"8\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"throughput\" data-i18n=\"throughput\"></label><output for=\"throughput\" id=\"throughput-value\"></output><input id=\"throughput\" type=\"range\" min=\"1\" max=\"8\" value=\"2\" step=\"1\"></div><div class=\"actions\"><button id=\"download\" data-i18n=\"download\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"history\"></h3><div id=\"history\"></div></section><div class=\"metrics\"><div><span data-i18n=\"buffer\"></span><strong id=\"buffer\"></strong></div><div><span data-i18n=\"stall\"></span><strong id=\"stall\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let buffer=6,stall=0,history=[],last='∅';function draw(){const r=+$('#bitrate').value,c=+$('#throughput').value;set('bitrate-value',r);set('throughput-value',c);intervals('history',Array.from({length:8},(_,i)=>({start:0,end:history[i]?.buffer||0,label:history[i]?'#'+(i+1)+' B='+history[i].buffer.toFixed(2)+' stall='+history[i].stall.toFixed(2):'#'+(i+1)+' ·',cls:history[i]?.stall?'gold':''})),22);set('buffer',buffer.toFixed(2));set('stall',stall.toFixed(2));set('equation','next download = 2×'+r+'/'+c+' = '+(2*r/c).toFixed(2)+' s\\n'+last);$('#download').disabled=history.length>=8;$('#history').dataset.values=JSON.stringify(history);}$('#download').onclick=()=>{const dt=2* +$('#bitrate').value/ +$('#throughput').value,old=buffer,s=Math.max(0,dt-buffer);buffer=Math.max(0,buffer-dt)+2;stall+=s;history.push({buffer,stall:s});last='B′ = max(0,'+old.toFixed(2)+' − '+dt.toFixed(2)+') + 2 = '+buffer.toFixed(2);draw()};$('#reset').onclick=()=>{buffer=6;stall=0;history=[];last='∅';draw()};document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"bitrate":"下一段码率(Mbit/s)","buffer":"当前缓冲(s)","download":"下载下一段","heading":"下载一段补两秒,为什么缓冲反而减少?","history":"每段完成后的水位","intro":"为下一段选择码率与吞吐,再下载;每次操作保留先前缓冲和累计卡顿。","reset":"重置","scope":"每段2s、固定码率,初始缓冲6s;无请求/解码开销、容量上限与自动ABR决策。下载按钮执行一次完整段,最多8次后重置。","scopeLabel":"模型范围与约定","stall":"累计卡顿(s)","throughput":"本次吞吐(Mbit/s)"}}
HLS:清单负责描述,媒体段负责内容
HLS 使用 UTF-8 播放清单,支持 MPEG-TS、fragmented MP4 等媒体形式,不能限定为 .ts。多变体清单列出可选表示;媒体清单描述具体媒体段。播放器也可以直接打开单一媒体清单,不必所有播放都先经过多变体层。HLS RFC 8216
下面是一个完整的短 fMP4 点播媒体清单示例;媒体文件需由兼容编码/打包器生成,这段文本本身不产生视频。
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="init.mp4"
#EXTINF:2.000,
seg-000.m4s
#EXTINF:2.000,
seg-001.m4s
#EXT-X-ENDLIST
EXT-X-MAP 指定初始化部分;EXTINF 表示该段时长。TARGETDURATION 的约束是段 EXTINF 时长按规范四舍五入后的整数不得超过它,不应把它笼统解释为所有段的精确长度。媒体序号标识播放清单中的段序列,不保证不同变体的同一序号能脱离时间戳直接互换。
| 清单类型/状态 | 更新规则与结果 |
| VOD | 内容固定,完整点播 |
| EVENT | 可向尾部追加,保留此前条目 |
| 滑动直播 | 新段追加,旧段可移出窗口 |
| ENDLIST | 不再添加媒体段;也可以出现在已结束的 EVENT/直播 |
低延迟 HLS:部分段、阻塞重载与预告配合
LL-HLS 让完整 segment 尚未结束时,已经生成的 partial segment 就能被发现和下载。阻塞播放清单重载允许客户端带 _HLS_msn / _HLS_part 请求未来进度,服务端等所需内容可用后返回,减少重复获取旧清单的等待。Apple LL-HLS
PRELOAD-HINT 提前告知可能需要的资源或范围,客户端可以先发请求;它不是“该资源现在已经完整存在”的保证,计划也可能改变。SERVER-CONTROL 宣告所支持能力,PART-INF 描述部分段时长参数;这些约定还需要源站、CDN 和播放器共同支持。Apple 阻塞重载讲解
缓存如果忽略 delivery directives,或代理一直缓冲响应到完整 segment 结束,源站即使快速发布也可能失去低延迟收益。更小的 part 增加请求和调度开销,缓冲更浅又降低抗抖动能力,因此 part 越小并不总是越好。
DASH 与 CMAF:表示、时间线和可共享媒体
DASH 的 MPD 使用 Period、AdaptationSet 和 Representation 组织媒体,SegmentTemplate 可以按编号或时间定位分段。timescale=90000、duration=180000 表示每段两秒;timescale 不是帧率。
动态 MPD 的时间模型涉及可用性起点、客户端时钟、时间移动窗口和播放延迟,minBufferTime 也不是“播放器必须固定缓冲这个秒数”的简单开关。DASH-IF 时间模型
低延迟 DASH 可以在一个 segment 尚在生成时增量传输 CMAF chunks。HTTP/1.1 可以采用 chunked transfer coding,HTTP/2/3 则按各自的数据帧传递,不能把 HTTP/1.1 的 chunked 编码列为所有 LL-DASH 的必要条件。DASH-IF 低延迟指南
CMAF 规范媒体组织与约束,不是另一种视频 codec。HLS 与 DASH 在 codec、轨道、分段边界及加密配置兼容时,可以复用媒体对象而使用不同清单;不符合这些条件时仍可能有多个包版本。“用了 CMAF 就一定缓存减半”忽略了实际封装与终端需求。
CDN:分发副本与源站负载的两本账
若一万名观众各看 3 Mbit/s,边缘到观众约需 30 Gbit/s 媒体吞吐;高缓存命中率可以减少回源,却不会让这一万份观众传输消失。假设二十个边缘各为同一档内容回源一份持续媒体,理想媒体回源约 60 Mbit/s,还未算其他档位、请求、预热与失败重试。
已完成且 URL 不复用的媒体段适合较长缓存;不断变化的直播清单需要及时更新。鉴权参数若让每个用户形成不同 cache key,会降低共享命中;忽略鉴权又可能越权。设计时要让边缘完成授权,并明确哪些内容可以共享,而不是简单给所有 URL 添加长 cache TTL。
RTMP(S) 与 SRT:回传侧的不同工具
RTMP 通过可靠字节流承载控制及媒体消息,其 chunk stream 用于消息分块和交错,不是 HLS 媒体 segment。RTMPS 增加 TLS 保护。Flash 浏览器播放退出历史,并不等于 RTMP 无法用于任何播放或现代回传;Enhanced RTMP 继续扩展编解码器与多轨能力,具体要看两端支持。Enhanced RTMP
SRT 使用 UDP 上的传输机制,实时模式通过丢包反馈/重传与时间戳交付缓冲兼顾恢复和及时性。SRTO_RCVLATENCY 与对端要求参与确定接收延迟约束;它们不是摄像机到显示屏总时延。数据即使早到,也可能按交付计划等待。SRT 延迟机制
提高延迟预算可以给重传更多机会,不能修复持续带宽不足。若编码 8 Mbit/s 而链路长期只有 6 Mbit/s,单纯加大缓冲只会推迟失败;重传还要额外占用带宽。Caller、Listener、Rendezvous 是建立连接的角色/方式,不是任何 NAT 都可穿越的保证。
互动与游戏串流:期限来自整条反馈路径
WebRTC 适合需要双向反馈的实时媒体,媒体服务器可以按订阅和层级分发;它与 HTTP 分段分发也可在同一平台中转换组合。不能把所有 WebRTC 连接固定描述为直连或同一种拥塞算法。
Sunshine/Moonlight 等游戏串流还包含输入回传、游戏逻辑与渲染、编码、下行、解码和显示,评估的是操作到画面的完整闭环。60 fps 只说明理想帧间隔约 16.7 ms,不证明整个闭环必须在一帧内完成,更不能断言任何重传都必然无用。FEC、重传、丢帧和帧节奏要在实际播放期限内权衡。
内容加密与播放授权
HTTPS 保护客户端到服务的传输;HLS AES-128 等方式保护媒体对象;DRM 还包括许可证和播放权限体系。三者不是同一个开关。使用 SAMPLE-AES 并不能单靠标签确定所有终端都会支持某个 DRM,密钥格式、加密模式与播放器能力都需匹配。
密钥 URL 可以下载不等于业务已经完成授权,URL 签名过期也不会抹除用户已经得到的内容。排查黑屏时应区分清单失败、媒体失败、初始化/codec 不支持和许可证失败,避免把所有问题归于网速。
选择方案时,用可复现指标描述需求:首次显示耗时、现场到播放延迟、卡顿比、画质变化、端点资源和单位观众传输成本。保留网络与编码条件后,测量才可以支持下一轮调整。