流媒体技术实战:从零搭建低延迟直播系统

Good“艳青 交互 阅读 2,383
赞 15 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

去年接了个直播带货的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分段切换的音频断层?这个我到现在还没搞定。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论