第五章:流媒体音频协议 —— 当音频不再是一个文件
前一章我们搞清楚了音频格式与编解码的取舍逻辑。但在实际产品中,用户很少会等一个 40MB 的音频文件完整下载后再开始播放。音乐流媒体、播客、在线电台、语音直播——这些场景的共同特点是:音频不是一次性交付的文件,而是一段持续流动的数据。支撑这一切的,就是流媒体协议。本章从协议原理到工程实战,帮你彻底搞清楚 Web 音频流媒体的技术选型。
一、为什么需要流媒体协议?
在传统的 <audio src="music.mp3"> 模式下,浏览器通过 HTTP 渐进式下载整个文件。这种方式对于短音频完全够用,但面对以下场景就力不从心了:
大文件点播:一个 2 小时的播客可能超过 100MB。用户只想听中间 5 分钟,却需要等待大量无用数据下载。虽然 HTTP Range 请求可以部分解决跳转问题,但无法做到自适应码率。
自适应码率(ABR):用户的网络状况随时在变——从 Wi-Fi 切到 4G、进电梯信号变差、多人共享带宽。如果始终用固定码率,要么高码率导致卡顿,要么低码率浪费了好网速。流媒体协议能让播放器实时根据带宽切换不同质量的音频。
直播场景:直播音频没有"完整文件"的概念,数据是实时产生、实时消费的。传统的文件下载模式根本无法工作,必须有一套"边生产边消费"的传输机制。
多语言/多音轨:一个节目可能有中文、英文两条音轨,或者带有音频描述轨道。流媒体协议通过 manifest 文件管理多条轨道,让播放器按需选择。
这些需求催生了 HLS、DASH 等现代流媒体协议。它们的核心思路惊人地一致:把音频切成小片段,用一个描述文件(manifest)告诉播放器这些片段在哪里、是什么质量。
二、流媒体传输的核心概念
在深入具体协议之前,先理解几个所有流媒体协议共享的基础概念。
分片(Segmentation)
将完整的音频流切割成若干个固定时长的小片段(通常 2~10 秒),每个片段是一个独立可解码的文件。分片是流媒体的基石——它使得播放器可以按需请求、随时切换码率、从任意位置开始播放。
$$\text{片段数量} = \frac{\text{总时长}}{\text{片段时长}} \quad \text{例:2小时节目 / 10秒 = 720 个片段}$$
分片时长的选择是一个权衡:
| 片段时长 | 优点 | 缺点 |
|---|---|---|
| 2s | 切换码率快,启动延迟低 | 片段数量多,manifest 大,请求频繁 |
| 6s | 平衡选择,多数场景适用 | — |
| 10s | 请求次数少,服务器压力小 | 码率切换慢,直播延迟高 |
清单文件(Manifest)
清单文件是播放器的"导航地图",描述了所有可用的码率版本和片段地址。HLS 用 .m3u8(文本格式),DASH 用 .mpd(XML 格式)。播放器先请求清单文件,再根据其中的信息按需请求片段。
自适应码率(ABR,Adaptive Bitrate)
播放器在播放过程中持续监测可用带宽和缓冲水位,动态在不同码率之间切换。一般策略是:
- 带宽估算:统计最近几个片段的下载速度
- 缓冲水位:缓冲区低于阈值时降码率,高于阈值时升码率
- 切换时机:在片段边界切换,对音频来说几乎无感知
$$\text{选择码率} \leq \text{可用带宽} \times \text{安全系数(通常 0.7~0.8)}$$
点播(VOD)vs 直播(Live)
| 维度 | 点播(VOD) | 直播(Live) |
|---|---|---|
| manifest | 固定不变,包含所有片段 | 动态更新,滑动窗口 |
| 片段 | 预先生成,存放在 CDN | 实时生成,持续追加 |
| Seek | 随意跳转 | 只能在缓冲窗口内跳转 |
| 延迟要求 | 无 | 关键指标(秒级到亚秒级) |
三、HLS 音频流深度解析
HLS(HTTP Live Streaming)由 Apple 在 2009 年推出,是目前使用最广泛的流媒体协议,尤其在 iOS/Safari 生态中是唯一原生支持的自适应流方案。
3.1 HLS 的工作原理
HLS 的传输流程可以分为三层:
┌──────────────────────────────────────────────┐
│ 编码器 / 转码服务 │
│ 将源音频编码并切成 .aac / .m4s 片段 │
└───────────────┬──────────────────────────────┘
│ 生成
▼
┌──────────────────────────────────────────────┐
│ M3U8 清单文件 │
│ 描述所有码率版本和片段地址 │
└───────────────┬──────────────────────────────┘
│ 请求
▼
┌──────────────────────────────────────────────┐
│ 播放器(浏览器 / hls.js) │
│ 解析清单 → 选择码率 → 按需请求片段 → 播放 │
└──────────────────────────────────────────────┘
3.2 M3U8 清单格式详解
HLS 使用两级清单结构:**主清单(Master Playlist)**列出所有码率版本,**媒体清单(Media Playlist)**列出某个码率版本的所有片段。
主清单(指向多个码率的媒体清单):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.2"
lo/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2"
mid/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=256000,CODECS="mp4a.40.2"
hi/playlist.m3u8
关键字段解读:
| 字段 | 含义 |
|---|---|
BANDWIDTH | 该码率版本的峰值比特率(bps),播放器据此做 ABR 决策 |
CODECS | 编码格式(mp4a.40.2 = AAC-LC) |
AVERAGE-BANDWIDTH | 平均比特率(可选,比 BANDWIDTH 更准确) |
媒体清单(某个码率版本的片段列表):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10.005,
segment_000.aac
#EXTINF:9.998,
segment_001.aac
#EXTINF:10.001,
segment_002.aac
#EXTINF:8.320,
segment_003.aac
#EXT-X-ENDLIST
关键标签含义:
| 标签 | 含义 |
|---|---|
#EXT-X-TARGETDURATION | 最大片段时长(秒),播放器用它计算缓冲策略 |
#EXT-X-MEDIA-SEQUENCE | 第一个片段的序号,直播场景中会递增 |
#EXT-X-PLAYLIST-TYPE | VOD(点播,完整列表)或 EVENT(直播,只追加不删除) |
#EXTINF | 下一个片段的实际时长(秒) |
#EXT-X-ENDLIST | 标记播放列表结束(直播流中没有此标签) |
3.3 用 FFmpeg 生成纯音频 HLS
# 单码率纯音频 HLS(适合播客)
ffmpeg -i podcast.mp3 \
-c:a aac -b:a 128k \
-hls_time 10 \
-hls_playlist_type vod \
-hls_segment_filename 'segment_%03d.aac' \
playlist.m3u8
# 多码率音频 HLS(自适应,适合音乐流媒体)
ffmpeg -i music.flac \
-map 0:a -c:a aac -b:a 64k \
-hls_time 10 \
-hls_segment_filename 'lo/seg_%03d.aac' \
lo/playlist.m3u8
ffmpeg -i music.flac \
-map 0:a -c:a aac -b:a 128k \
-hls_time 10 \
-hls_segment_filename 'mid/seg_%03d.aac' \
mid/playlist.m3u8
ffmpeg -i music.flac \
-map 0:a -c:a aac -b:a 256k \
-hls_time 10 \
-hls_segment_filename 'hi/seg_%03d.aac' \
hi/playlist.m3u8FFmpeg HLS 关键参数说明:
| 参数 | 含义 |
|---|---|
-hls_time 10 | 目标片段时长 10 秒 |
-hls_playlist_type vod | 生成 VOD 类型清单(包含 #EXT-X-ENDLIST) |
-hls_segment_filename | 片段文件命名模板 |
-hls_segment_type fmp4 | 使用 fMP4 格式片段(默认是 MPEG-TS) |
-hls_flags independent_segments | 每个片段可独立解码 |
3.4 前端接入 HLS
Safari 原生支持 HLS,其他浏览器需要 hls.js(底层基于 MSE,第六章详解):
import Hls from 'hls.js';
function playHLSAudio(audioEl, url) {
// Safari 原生支持 HLS
if (audioEl.canPlayType('application/vnd.apple.mpegurl')) {
audioEl.src = url;
return null;
}
// 其他浏览器用 hls.js(基于 MSE)
if (Hls.isSupported()) {
const hls = new Hls({
maxBufferLength: 60, // 最大前向缓冲 60 秒
maxMaxBufferLength: 120, // 绝对上限 120 秒
startLevel: -1, // -1 = 自动选择起始码率
abrEwmaDefaultEstimate: 500000, // 初始带宽估算(bps)
});
hls.loadSource(url);
hls.attachMedia(audioEl);
// 监听码率切换事件
hls.on(Hls.Events.LEVEL_SWITCHING, (event, data) => {
console.log(`码率切换 → Level ${data.level}:`,
hls.levels[data.level].bitrate, 'bps');
});
// 错误处理
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('网络错误,尝试恢复');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('媒体错误,尝试恢复');
hls.recoverMediaError();
break;
default:
console.error('不可恢复的错误');
hls.destroy();
}
}
});
return hls;
}
console.error('当前浏览器不支持 HLS 播放');
return null;
}3.5 HLS 直播清单
直播场景中,媒体清单没有 #EXT-X-ENDLIST,播放器需要定期重新请求清单获取新片段:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:1042
#EXTINF:10.005,
live_seg_1042.aac
#EXTINF:9.998,
live_seg_1043.aac
#EXTINF:10.001,
live_seg_1044.aac
每隔 #EXT-X-TARGETDURATION 秒,播放器重新请求这个清单,发现 MEDIA-SEQUENCE 增加了,就知道有新片段可以下载。旧片段从清单中移除(滑动窗口),保持清单大小不会无限增长。
四、DASH 音频流深度解析
DASH(Dynamic Adaptive Streaming over HTTP)是 MPEG 组织制定的国际标准(ISO/IEC 23009-1),被 YouTube、Netflix、Spotify 等大规模采用。
4.1 DASH vs HLS 的核心差异
| 维度 | HLS | DASH |
|---|---|---|
| 制定者 | Apple | MPEG(国际标准) |
| 清单格式 | .m3u8(文本) | .mpd(XML) |
| 片段格式 | MPEG-TS 或 fMP4 | fMP4(主流)或 WebM |
| Safari 原生支持 | ✅ | ❌ |
| DRM | FairPlay | Widevine / PlayReady |
| 音视频分轨 | 支持(#EXT-X-MEDIA) | 原生设计(AdaptationSet) |
| 时间精度 | 秒级 | 毫秒级 |
一句话总结:HLS 在 Apple 生态是首选;DASH 是更开放、更灵活的国际标准,在 Android 和 Web 端更主流。
4.2 MPD 清单格式详解
DASH 的 MPD(Media Presentation Description)用 XML 描述,结构比 M3U8 更复杂但也更强大:
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
type="static"
mediaPresentationDuration="PT45M30S"
minBufferTime="PT6S"
profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">
<Period id="1" duration="PT45M30S">
<!-- 音频自适应集 -->
<AdaptationSet mimeType="audio/mp4" codecs="mp4a.40.2"
lang="zh" segmentAlignment="true">
<!-- 低码率 -->
<Representation id="audio-lo" bandwidth="64000" audioSamplingRate="44100">
<AudioChannelConfiguration
schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
value="2"/>
<BaseURL>audio_64k/</BaseURL>
<SegmentTemplate
initialization="init.mp4"
media="seg_$Number%04d$.m4s"
startNumber="1"
duration="10000"
timescale="1000"/>
</Representation>
<!-- 中码率 -->
<Representation id="audio-mid" bandwidth="128000" audioSamplingRate="44100">
<AudioChannelConfiguration
schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
value="2"/>
<BaseURL>audio_128k/</BaseURL>
<SegmentTemplate
initialization="init.mp4"
media="seg_$Number%04d$.m4s"
startNumber="1"
duration="10000"
timescale="1000"/>
</Representation>
<!-- 高码率 -->
<Representation id="audio-hi" bandwidth="256000" audioSamplingRate="44100">
<AudioChannelConfiguration
schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
value="2"/>
<BaseURL>audio_256k/</BaseURL>
<SegmentTemplate
initialization="init.mp4"
media="seg_$Number%04d$.m4s"
startNumber="1"
duration="10000"
timescale="1000"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>MPD 核心概念层级:
| 层级 | 含义 | 音频场景示例 |
|---|---|---|
MPD | 整个媒体描述的根节点 | — |
Period | 一个时间段(广告插入点) | 正片 / 广告 |
AdaptationSet | 一组可互相替换的表示(同一内容的不同码率) | 中文音轨 / 英文音轨 |
Representation | 一个具体的编码版本 | 64kbps AAC / 128kbps AAC |
SegmentTemplate | 片段 URL 生成规则 | seg_0001.m4s, seg_0002.m4s ... |
4.3 用 FFmpeg 生成 DASH 音频
# 生成纯音频 DASH(多码率)
ffmpeg -i music.flac \
-map 0:a -c:a aac -b:a 64k -f dash -init_seg_name 'lo/init.mp4' -media_seg_name 'lo/seg_$Number%04d$.m4s' \
-map 0:a -c:a aac -b:a 128k -f dash -init_seg_name 'mid/init.mp4' -media_seg_name 'mid/seg_$Number%04d$.m4s' \
-map 0:a -c:a aac -b:a 256k -f dash -init_seg_name 'hi/init.mp4' -media_seg_name 'hi/seg_$Number%04d$.m4s' \
-seg_duration 10 \
-use_template 1 \
-use_timeline 0 \
manifest.mpd4.4 前端接入 DASH
DASH 在浏览器端通常使用 dash.js(DASH 行业论坛官方参考播放器)或 Shaka Player(Google 开源):
import dashjs from 'dashjs';
function playDASHAudio(audioEl, mpdUrl) {
const player = dashjs.MediaPlayer().create();
player.initialize(audioEl, mpdUrl, /* autoPlay */ false);
// 配置 ABR 策略
player.updateSettings({
streaming: {
abr: {
initialBitrate: { audio: 128 }, // 初始码率(kbps)
autoSwitchBitrate: { audio: true }, // 自动切换
},
buffer: {
bufferTimeAtTopQuality: 30, // 最高码率时的缓冲目标(秒)
bufferTimeAtTopQualityLongForm: 60, // 长内容的缓冲目标
},
},
});
// 监听码率切换
player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, (e) => {
if (e.mediaType === 'audio') {
console.log('音频码率切换 →', e.newQuality);
}
});
return player;
}五、Icecast 在线电台
在 HLS/DASH 统治流媒体之前,互联网电台靠的是 Icecast / SHOUTcast 这类传统流媒体服务器。它们的原理更简单粗暴:通过一个永不结束的 HTTP 响应持续发送音频数据。
5.1 Icecast 工作原理
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 音频源 │ ───→ │ Icecast │ ───→ │ 浏览器 │
│ (DJ 软件 / │ │ 服务器 │ │ <audio> │
│ 自动播放列表)│ │ 分发给所有 │ │ 直接播放 │
└──────────────┘ │ 连接的客户端 │ └──────────────┘
└──────────────┘
浏览器可以直接用 <audio> 播放 Icecast 流,无需任何 JavaScript 库——因为 Icecast 发送的就是标准的 MP3 或 AAC 数据流:
<audio src="https://radio.example.com:8000/stream" controls></audio>5.2 获取电台元数据
Icecast 流本身携带的是纯音频数据,当前曲目信息需要通过 Icecast 的 JSON 状态接口获取:
async function getStreamMetadata(stationUrl) {
const statusUrl = stationUrl.replace('/stream', '/status-json.xsl');
const res = await fetch(statusUrl);
const data = await res.json();
const source = data.icestats?.source;
return {
title: source?.title || '未知曲目',
artist: source?.artist || '',
listeners: source?.listeners || 0,
bitrate: source?.bitrate || 0,
genre: source?.genre || '',
};
}
// 定期轮询元数据(曲目切换时更新)
function pollMetadata(stationUrl, interval = 10000) {
let currentTitle = '';
setInterval(async () => {
const meta = await getStreamMetadata(stationUrl);
if (meta.title !== currentTitle) {
currentTitle = meta.title;
updateNowPlaying(meta); // 更新 UI
}
}, interval);
}5.3 Icecast 的局限性
| 维度 | Icecast | HLS/DASH |
|---|---|---|
| 自适应码率 | ❌ 不支持 | ✅ 核心特性 |
| CDN 分发 | 困难(长连接) | 简单(标准 HTTP 文件) |
| Seek 跳转 | ❌ 只能实时收听 | ✅ 支持(VOD) |
| 延迟 | 极低(2~5s) | 较高(6~30s) |
| 适用场景 | 小型电台、内部广播 | 大规模流媒体分发 |
Icecast 最大的优势是低延迟和极简接入,最大的劣势是无法利用 CDN 横向扩展。对于听众超过几百人的场景,通常会用 HLS 替代。
六、播客 RSS 与音频分发
播客(Podcast)是音频内容分发的一个独特场景。它不走流媒体协议,而是基于古老但可靠的 RSS 2.0 + iTunes 扩展标准。
6.1 播客 RSS 格式
播客客户端(Apple Podcasts、Spotify、小宇宙等)通过订阅一个 RSS Feed URL 来获取节目列表和音频地址:
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
xmlns:podcast="https://podcastindex.org/namespace/1.0"
version="2.0">
<channel>
<title>Web 音频技术播客</title>
<description>深入 Web Audio API 与音频工程</description>
<language>zh-cn</language>
<itunes:author>白川</itunes:author>
<itunes:image href="https://example.com/cover.jpg"/>
<itunes:category text="Technology"/>
<item>
<title>第 1 期:Web Audio API 入门</title>
<description>从 AudioContext 到完整音频图...</description>
<enclosure
url="https://cdn.example.com/ep01.mp3"
length="45000000"
type="audio/mpeg"/>
<itunes:duration>45:32</itunes:duration>
<itunes:episode>1</itunes:episode>
<itunes:season>1</itunes:season>
<pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate>
<podcast:chapters url="https://cdn.example.com/ep01-chapters.json"
type="application/json+chapters"/>
</item>
<item>
<title>第 2 期:音频可视化实战</title>
<enclosure
url="https://cdn.example.com/ep02.mp3"
length="52000000"
type="audio/mpeg"/>
<itunes:duration>54:10</itunes:duration>
<itunes:episode>2</itunes:episode>
<pubDate>Mon, 08 Jan 2024 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>关键标签说明:
| 标签 | 含义 |
|---|---|
<enclosure> | 音频文件的 URL、大小和 MIME 类型(播客的核心) |
<itunes:duration> | 节目时长(客户端展示用) |
<itunes:image> | 节目/单集封面 |
<podcast:chapters> | 章节标记文件(Podcasting 2.0 标准) |
6.2 前端解析播客 RSS
async function parsePodcastFeed(rssUrl) {
const text = await fetch(rssUrl).then(r => r.text());
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/xml');
const channel = doc.querySelector('channel');
const items = Array.from(doc.querySelectorAll('item')).map(item => ({
title: item.querySelector('title')?.textContent,
desc: item.querySelector('description')?.textContent,
audioUrl: item.querySelector('enclosure')?.getAttribute('url'),
fileSize: parseInt(item.querySelector('enclosure')?.getAttribute('length') || '0'),
mimeType: item.querySelector('enclosure')?.getAttribute('type'),
duration: item.querySelector('itunes\\:duration, duration')?.textContent,
date: item.querySelector('pubDate')?.textContent,
episode: item.querySelector('itunes\\:episode')?.textContent,
}));
return {
title: channel?.querySelector('title')?.textContent,
description: channel?.querySelector('description')?.textContent,
author: channel?.querySelector('itunes\\:author')?.textContent,
coverUrl: channel?.querySelector('itunes\\:image')?.getAttribute('href'),
items,
};
}
// 使用
const feed = await parsePodcastFeed('https://example.com/podcast/feed.xml');
console.log(`${feed.title} - ${feed.items.length} 集`);
feed.items.forEach(ep => {
console.log(`${ep.title} (${ep.duration}) → ${ep.audioUrl}`);
});6.3 播客音频的格式选择
播客与音乐流媒体不同,内容以人声为主,对高频细节要求不高,但对文件体积和兼容性要求极高(需要兼容所有播客客户端):
| 格式 | 推荐码率 | 4 分钟体积 | 兼容性 | 推荐场景 |
|---|---|---|---|---|
| MP3 128 kbps | 128k | ~3.7 MB | ✅ 全平台 | 最安全的选择,所有客户端都支持 |
| MP3 64 kbps(单声道) | 64k | ~1.8 MB | ✅ 全平台 | 对话类播客(体积最小) |
| AAC 64 kbps | 64k | ~1.8 MB | ⚠️ 部分客户端 | 音质优于同码率 MP3 |
| Opus 48 kbps | 48k | ~1.4 MB | ❌ 有限 | 技术上最优,但兼容性不够 |
Apple Podcasts 官方推荐:MP3(CBR 128 kbps 立体声)或 AAC(64 kbps 立体声)。绝大多数播客至今仍然使用 MP3,因为它的兼容性无可替代。
七、音频直播方案全景对比
音频直播的技术选型,核心考量是延迟。不同场景对延迟的容忍度天差地别:
| 场景 | 可接受延迟 | 推荐方案 |
|---|---|---|
| 音乐电台/背景播放 | 10~30s | 标准 HLS |
| 播客直播/语音房 | 2~6s | Low-Latency HLS / LL-DASH |
| 连麦互动/语音通话 | < 500ms | WebRTC |
| 游戏语音/指挥 | < 200ms | WebRTC |
7.1 标准 HLS 直播
延迟 = 片段时长 × 缓冲片段数 + 编码延迟。典型配置下(10s 片段 × 3 个缓冲),延迟约 30 秒。
# FFmpeg 推送 HLS 直播流
ffmpeg -f avfoundation -i ":0" \
-c:a aac -b:a 128k \
-hls_time 6 \
-hls_list_size 5 \
-hls_flags delete_segments \
-hls_segment_filename 'live/seg_%05d.aac' \
live/stream.m3u87.2 Low-Latency HLS(LL-HLS)
Apple 在 2019 年推出 LL-HLS,通过部分片段(Partial Segments)和阻塞式清单请求将延迟降到 2~6 秒:
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:6
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.5
#EXTINF:6.000,
seg_100.aac
#EXT-X-PART:DURATION=1.0,URI="seg_101_part0.aac"
#EXT-X-PART:DURATION=1.0,URI="seg_101_part1.aac"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg_101_part2.aac"
核心改进:
- 部分片段(Part):一个完整片段(6s)被进一步细分为多个 Part(1s),播放器不用等完整片段生成就能开始下载
- 阻塞式清单请求:播放器在请求清单时带上参数,服务器在有新 Part 可用之前不返回响应,避免无效轮询
- 预加载提示(Preload Hint):告诉播放器下一个即将可用的 Part 地址,提前建立连接
7.3 WebRTC 实时音频
WebRTC 是延迟最低的方案(< 500ms),但架构复杂度也最高。它不走 HTTP,而是基于 UDP + SRTP 直接传输音频数据:
// 简化的 WebRTC 音频收听端
async function listenToStream(signalingUrl) {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// 接收远端音频轨道
pc.ontrack = (event) => {
const audio = document.getElementById('audioPlayer');
audio.srcObject = event.streams[0];
audio.play();
};
// 通过信令服务器交换 SDP
const ws = new WebSocket(signalingUrl);
ws.onmessage = async (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'offer') {
await pc.setRemoteDescription(data);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify(answer));
}
if (data.candidate) {
await pc.addIceCandidate(data.candidate);
}
};
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({ candidate: event.candidate }));
}
};
}7.4 WebSocket + Web Audio API
一种折中方案——延迟比 HLS 低(1~3s),比 WebRTC 简单。通过 WebSocket 传输编码后的音频数据块,用 Web Audio API 解码播放:
class WSAudioPlayer {
constructor(wsUrl) {
this.ctx = new AudioContext();
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
this._nextTime = 0;
this._startOffset = 0.1; // 预缓冲 100ms
}
start() {
this.ws.onmessage = async (event) => {
try {
const audioBuffer = await this.ctx.decodeAudioData(event.data);
this._scheduleBuffer(audioBuffer);
} catch (err) {
console.warn('解码失败,跳过此块:', err);
}
};
}
_scheduleBuffer(buffer) {
const source = this.ctx.createBufferSource();
source.buffer = buffer;
source.connect(this.ctx.destination);
const now = this.ctx.currentTime;
const startTime = Math.max(now + this._startOffset, this._nextTime);
source.start(startTime);
this._nextTime = startTime + buffer.duration;
}
stop() {
this.ws.close();
this.ctx.close();
}
}注意:
decodeAudioData需要接收完整的可解码音频块(如一个完整的 MP3 帧或 AAC 帧),不能是任意长度的字节流。服务端需要按帧边界切割数据。
八、实战:Web 音频流媒体协议选型策略
8.1 按场景选协议
音乐流媒体平台(Spotify 模式):
点播:HLS + 多码率(64k / 128k / 256k / 320k)
→ Safari 原生 + hls.js 兜底
→ 或 DASH + Shaka Player
离线:下载完整文件,用 <audio> 直接播放
播客平台(小宇宙模式):
分发:RSS Feed + CDN 托管 MP3 文件
播放:<audio src="..."> 渐进式下载即可
增强:记录播放位置 + 倍速播放 + 章节跳转
在线电台(喜马拉雅 FM 模式):
直播:HLS(6s 片段,延迟 18~30s,可接受)
→ 大量听众场景,CDN 友好
互动:WebRTC(连麦场景,延迟 < 500ms)
回放:转为 VOD HLS
语音社交/Clubhouse 模式:
房间音频:WebRTC(多人低延迟)
旁听模式:HLS 转推(降低服务器压力)
游戏内语音:
WebRTC(Opus 编码,超低延迟)
8.2 协议能力总览
| 特性 | HLS | DASH | Icecast | WebRTC | WebSocket |
|---|---|---|---|---|---|
| 自适应码率 | ✅ | ✅ | ❌ | ❌ | ❌(需自实现) |
| CDN 友好 | ✅ | ✅ | ❌ | ❌ | ❌ |
| Safari 原生 | ✅ | ❌ | ✅ | ✅ | — |
| 延迟 | 6~30s | 2~10s | 2~5s | < 500ms | 1~3s |
| DRM | FairPlay | Widevine | ❌ | ❌ | ❌ |
| 实现复杂度 | 低 | 中 | 极低 | 高 | 中 |
| 直播支持 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 点播支持 | ✅ | ✅ | ❌ | ❌ | ❌ |
8.3 音频流媒体的 CDN 部署
HLS/DASH 的最大优势之一是对 CDN 天然友好——所有数据都是标准的 HTTP 静态文件:
源站(Origin) CDN 边缘节点 客户端
manifest.mpd ──缓存──→ manifest.mpd ──请求──→ 播放器
seg_001.m4s ──缓存──→ seg_001.m4s ──请求──→
seg_002.m4s ──缓存──→ seg_002.m4s ──请求──→
缓存策略建议:
| 文件类型 | 缓存时间 | 原因 |
|---|---|---|
| 主清单(Master) | 长期(1 天+) | 码率列表不常变 |
| 媒体清单(VOD) | 长期(1 天+) | 点播内容不变 |
| 媒体清单(Live) | 不缓存或极短(1s) | 需要实时更新 |
| 媒体片段 | 长期(1 天+) | 片段内容不变 |
九、常见问题与排查
"HLS 直播延迟太高"
默认 HLS 延迟 = hls_time × 3 + 编码延迟。降低延迟的方法:
- 缩短片段时长(
-hls_time 2),但会增加请求频率 - 减少缓冲片段数(hls.js 配置
liveSyncDurationCount: 2) - 升级到 LL-HLS
"DASH 在 Safari 上不播放"
Safari 不原生支持 DASH。方案:
- 同时提供 HLS 和 DASH,按浏览器分流
- 使用同时支持两种协议的播放器(如 Shaka Player)
"Icecast 流在移动端自动断开"
移动浏览器在后台会断开长连接。方案:
- 使用 Service Worker 保持连接
- 监听
visibilitychange事件,页面恢复时重连 - 考虑迁移到 HLS(片段式请求,对后台更友好)
"WebSocket 音频播放有间断"
通常是因为网络抖动导致数据到达不及时。方案:
- 增加预缓冲时间(
_startOffset) - 实现 jitter buffer(抖动缓冲区)
- 监测
_nextTime与currentTime的差值,动态调整
十、本章知识图谱
小结
流媒体协议是音频从"文件"进化为"服务"的关键技术层。HLS 和 DASH 通过分片 + 清单 + ABR 的架构,让音频能在不同网络条件下自适应播放;Icecast 以极简的方式支撑了互联网电台的黄金年代;播客 RSS 用最朴素的分发协议养活了整个播客生态;WebRTC 则把延迟压到了亚秒级,让实时语音成为可能。
没有"最好的协议",只有"最适合场景的协议"——这和上一章音频格式的选择逻辑如出一辙。理解每种协议的延迟特性、扩展能力和实现成本,才能在架构设计时做出正确的技术决策。
下一章我们将深入 Media Source Extensions(MSE)——hls.js 和 dash.js 之所以能在不支持 HLS/DASH 的浏览器上工作,正是因为 MSE 提供了"用 JavaScript 直接向媒体元素喂数据"的底层能力。理解 MSE,就理解了整个 Web 流媒体技术栈的发动机。