流媒体技术实战:从零搭建低延迟直播系统
项目初期的技术选型
去年接了个直播带货的H5页面,客户要求在移动端能实时播放商品讲解视频,延迟尽量低。一开始我直接上<video>标签,结果发现普通MP4根本扛不住——首屏加载要七八秒,用户早划走了。后来才意识到这得用流媒体。
对比了HLS和WebRTC,WebRTC延迟确实低(1秒内),但兼容性太差,安卓低端机一堆问题,还得搭信令服务器,工期来不及。最后选了HLS,虽然延迟5-8秒,但iOS原生支持,安卓用hls.js也能跑,方案最稳。当时想:先让功能跑起来,再优化体验吧。
最大的坑:安卓低端机卡成PPT
本地测试没问题,一上真机就翻车。红米Note 8这种千元机,播放1080p HLS流直接卡顿掉帧,CPU占用飙到90%。查了下原因:HLS本质是切片TS文件,每2秒一个分片,低端机解码+网络请求双重压力,根本扛不住。
折腾了半天发现,关键不是代码逻辑,而是分辨率和码率没做适配。我一股脑推了1080p,但实际用户很多在4G弱网环境。后来加了动态码率切换:
// 根据网络状态自动切换清晰度
const video = document.getElementById('live-video');
const hls = new Hls();
if (Hls.isSupported()) {
// 先加载低码率流保底
hls.loadSource('https://jztheme.com/stream/low.m3u8');
hls.attachMedia(video);
// 监听网络变化
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
connection.addEventListener('change', () => {
const effectiveType = connection.effectiveType; // '4g', '3g', '2g'
if (effectiveType === '4g') {
hls.loadSource('https://jztheme.com/stream/high.m3u8'); // 切高清
} else {
hls.loadSource('https://jztheme.com/stream/low.m3u8'); // 切标清
}
});
}
}
这招亲测有效,卡顿率从40%降到10%以下。不过有个小问题:切换时会有1-2秒黑屏,因为hls.js要重新加载manifest。时间紧就没深究,反正用户能接受短暂中断。
核心代码就这几行,但细节魔鬼
很多人以为HLS接入就是引入hls.js然后loadSource,其实坑都在细节里。比如iOS Safari必须用原生<video>,不能用hls.js;而安卓又必须用hls.js,否则直接播不了。我一开始统一用hls.js,结果iOS白屏,差点背锅。
最终方案得做平台判断:
const video = document.getElementById('live-video');
const streamUrl = 'https://jztheme.com/stream/index.m3u8';
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// iOS Safari 原生支持
video.src = streamUrl;
} else if (Hls.isSupported()) {
// 安卓及其他浏览器
const hls = new Hls();
hls.loadSource(streamUrl);
hls.attachMedia(video);
} else {
// 降级提示
alert('您的浏览器不支持直播播放');
}
这里注意我踩过好几次坑:必须先检查canPlayType再初始化hls.js。有次我把hls.js初始化放在前面,iOS虽然能播,但控制栏样式全乱了,因为hls.js劫持了video元素。
还有个小细节:HLS流地址必须HTTPS,否则现代浏览器直接拦截。测试时用localhost没事,一上生产环境就报错,排查半天才发现Nginx没配SSL证书。这种低级错误真的会让人半夜惊醒。
回放功能差点搞崩服务器
客户临时加需求:直播结束后要能回看。我以为直接存TS分片就行,结果发现HLS回放需要完整的m3u8索引文件。更麻烦的是,如果直播持续2小时,m3u8文件会无限增长,内存直接爆掉。
后来用FFmpeg做了分段录制:每30分钟生成一个独立的m3u8+TS包。前端回放时拼接多个m3u8:
// 回放模式:按时间段加载不同m3u8
function loadReplay(startTime, endTime) {
const segments = generateSegmentUrls(startTime, endTime); // 返回['/replay/1.m3u8', '/replay/2.m3u8'...]
let current = 0;
function playNext() {
if (current >= segments.length) return;
hls.loadSource(segments[current]);
hls.on(Hls.Events.BUFFER_EOS, () => {
current++;
playNext(); // 播完自动切下一段
});
}
playNext();
}
这方案虽然糙,但避免了单个m3u8过大。不过有个遗留问题:段与段切换时有0.5秒音频断层,因为TS分片不是严格对齐的。跟后端对齐切割时间戳能解决,但排期满了就没改,反正用户反馈“基本能用”。
回顾与反思
整体效果还行:上线后直播页跳出率从65%降到35%,卡顿投诉很少。做得好的地方是动态码率切换和平台兼容处理,这两点救了项目。但也有明显不足:
- 没做预加载:用户点开直播要等3秒才出画面,应该提前加载第一个TS分片
- 错误重试机制弱:网络抖动时直接黑屏,应该加自动重连(比如失败3次后切备用流)
- 电量消耗大:长时间播放发热严重,可能跟hls.js的缓存策略有关,但没时间深挖
说到底,流媒体在H5上还是妥协的艺术。HLS延迟高、WebRTC兼容差,没有完美方案。这次选HLS是对的,毕竟稳定压倒一切。如果重来一次,我会在项目初期就压测低端机,而不是等QA提bug才救火。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如怎么优雅解决m3u8分段切换的音频断层?这个我到现在还没搞定。

暂无评论