☰
uniapp+mui-player适配m3u8/flv直播流与多端差异处理
2026/11/2 8:27:31 网站建设 项目流程

做直播播放这块的时间长了,慢慢会发现一个规律:真正难的从来不是“把流播出来”,而是“在五花八门的端上都能稳定播出来”。同一个 m3u8 地址,在 PC 端 Chrome 里丝滑得很,换个安卓机型的 WebView 就开始无限转圈;同一个 http-flv 流,桌面端跑得好好的,一到 iOS 上直接黑屏给你看。这次要聊的,就是我在 uniapp 项目里用mui-player插件去适配m3u8 / flv 直播流的整套做法,以及移动端和 PC 端那些不得不单独处理的差异点。

如果你正在用 uniapp 做直播、监控、在线教育、赛事转播这类带实时视频的场景,并且已经被“iOS 能播安卓不能播”“PC 正常手机上黑屏”“切后台回来就卡死”折磨过,这篇内容应该能帮你少走不少弯路。我会把选型逻辑、协议与端的支持边界、参数调优的计算过程、销毁与重连的写法、以及一堆排查实录都摊开讲,代码可以直接抄,但更重要的是抄完之后你知道为什么要这么写。

1. 先把方案定下来:为什么 uniapp 里选 mui-player

动手写代码之前,值得先花十分钟把选型想清楚。uniapp 这个框架的特殊性在于,一份代码要跑在 H5、App(vue 页面由 WebView 渲染)、App(nvue 原生渲染)、各家小程序等多个容器里,而视频播放恰恰是各端能力差异最大的模块之一。选错播放器,后面全是补丁摞补丁。

1.1 三个候选方案的取舍逻辑

uniapp 生态里做视频播放,主流的就那么几条路,我把当时评估的结论摆出来。

第一条路是直接用原生<video>标签。零依赖、体积小,但它的能力边界非常窄:mp4 稳,m3u8 在 iOS Safari 和部分安卓浏览器上能播,flv 基本没戏(除非是特定内核)。它没有统一的错误处理、没有码率切换、没有重连机制,一旦流断了你只能靠监听error事件自己从头搓一套逻辑。用在“偶尔播个宣传片”的场景够了,做直播是自找麻烦。

第二条路是live-player组件。这是 uniapp 提供的原生组件,性能和耗电表现最好,支持 rtmp、flv、hls(hls 的支持度视版本而定)。但它有两个硬门槛:一是小程序端需要相应的类目资质才能用,二是它的 UI 完全自定义,控制条、手势、清晰度面板都得自己画。App 端可以用,H5 端不支持。如果你的产品形态确定只做 App 和小程序,这条路其实最省心;但只要沾上 H5,方案就得拆成两套。

第三条路就是基于 MSE(Media Source Extensions)的 Web 播放器,mui-player 属于这一档。它的本质是一个 Vue 组件封装,把 video 元素、控制条 UI、以及 hls.js / flv.js / dash.js 这几个解码内核串起来,对外暴露一套统一的 API 和配置项。选它的理由有三个:一份代码同时覆盖 H5 和 App 的 vue 页面(因为 App 端 vue 页面跑在 WebView 里,DOM 是完整的),m3u8 和 flv 都能走同一套组件,UI 和交互开箱即用,省掉自己画控制条的时间。

代价也很明确:它离不开 DOM,所以小程序端和 nvue 页面直接出局;它依赖 MSE,所以那些不支持 MSE 的老设备天然播不了 flv。这两条限制我在项目一开始就写进了技术方案里,后面所有的“差异处理”其实都是在给这两条限制打补丁。

1.2 协议与端的支持矩阵

这张表是整个方案的地基,建议你先对着自己的目标设备过一遍,再决定要不要往下读。

运行环境渲染方式DOM 可用m3u8(HLS)flv(HTTP-FLV)结论
H5 / PC Chrome、Edge、Firefox浏览器是走 hls.js走 flv.js都可用,优先 flv 降延迟
H5 / PC Safari浏览器是原生支持,直给 srcMSE 支持不完整,不靠谱只能走 HLS
H5 / iOS Safari、iOS 内嵌 WebViewWebKit是原生支持,直给 src基本不可用必须走 HLS
H5 / 安卓 Chrome、国产内核 WebView浏览器 / WebView是走 hls.js(需 MSE)走 flv.js(需 MSE)需做 MSE 探测
App vue 页面(iOS)WebView是原生支持不可用必须走 HLS
App vue 页面(安卓)WebView是走 hls.js走 flv.js需做 MSE 探测
App nvue 页面原生渲染否不支持不支持换 live-player 或原生插件
各家小程序原生组件否需 live-player 或 web-view同左不在本文方案内

从表里能读出一条清晰的结论:m3u8 是全端通吃的最大公约数,flv 是“能用就用”的加速选项。所以我的整体策略是“以 HLS 为基线,以 FLV 为增强”:服务端两路流都推,客户端根据设备能力择优选用,flv 播不了就自动回落到 m3u8。

注意:别指望“一份流地址通吃所有端”。如果业务方只能提供 flv 地址,那 iOS 端就是无解的,必须在服务端补一路 HLS 转封装。这个事在需求评审阶段就要顶回去,等到联调时才发现,返工成本会很高。

1.3 我最终落地的分层结构

真实项目里我不会把播放逻辑散落在各个页面,而是抽成一个可复用的播放器封装层,大概分三层。

最底下是端能力探测层,负责判断当前环境:是不是 iOS、支不支持 MSE、是不是在某个定制内核的 WebView 里、当前网络类型是什么。这一层输出一个能力对象,后面所有决策都基于它。

中间是协议决策层,根据能力对象和拿到的流地址列表,决定用哪个地址、注册哪个内核、用哪套参数。比如 iOS 直接video.src = m3u8,安卓则new Hls()挂上去。

最上面是组件封装层,把 mui-player 的实例、生命周期、销毁、重连、事件回调都收进来,页面只需要传一个流地址和几个开关,别的都不管。页面切换时统一调用封装层的destroy,避免后台偷偷拉流。

这个分层看起来有点小题大做,但等你遇到“某个页面忘了销毁导致两个实例抢同一个流”“iOS 上重复调用 play 触发 AbortError”这类问题时,就会发现有个统一出口有多重要。

2. 集成落地:从装包到第一个能播的画面

选型定完,接下来是把它跑起来。这部分看着简单,实际上版本选择和实例生命周期是两个最容易翻车的地方。

2.1 版本锁定与依赖清单

依赖一共四个:mui-player本体、hls.js、flv.js,以及可选的dash.js(用不上就别装,白增体积)。

npm i mui-player hls.js flv.js

版本这块我必须强调一句:hls.js 的大版本跨度很大,1.x 和 0.14.x 的 API、内部行为、对 MSE 的要求都不一样。mui-player 的扩展插件是按某个版本区间写的适配,两者版本错配时最典型的报错是xxx is not a constructor或者干脆静默不播。我的做法是先把 mui-player 锁死在一个小版本(~前缀),再按它的 README 推荐的 hls.js / flv.js 版本去对,装完之后一定跑一遍真机验证,别只在模拟器上看。

另外提醒一句关于构建的:flv.js的老版本里有对global的引用,在 Vue3 + Vite 的构建链路下可能报global is not defined。遇到这种情况,在vite.config.js的define里补一个global: 'globalThis',或者在index.html里提前挂一个window.global = window兜底。这个坑我在两个项目里都踩到过,表现是本地 dev 正常、打包后白屏,非常迷惑。

2.2 最小可运行实例

先看一段能跑起来的最简代码,我用的是 uniapp 的 vue 页面结构。

<template> <view class="player-wrap"> <view id="mp-container" class="mp-box"></view> </view> </template> <script> import MuiPlayer from 'mui-player' import 'mui-player/dist/mui-player.min.css' import Hls from 'hls.js' import Flv from 'flv.js' export default { data() { return { mp: null } }, mounted() { this.$nextTick(() => { this.initPlayer() }) }, methods: { initPlayer() { MuiPlayer.use(Hls, Flv) // 注册扩展内核,具体签名以你安装版本的 README 为准 this.mp = new MuiPlayer({ container: '#mp-container', src: 'https://your-domain/live/stream.m3u8', type: 'hls', autoplay: true, live: true, height: '100%', width: '100%', themeColor: '#2b7cf6', language: 'zh-cn', videoInit: true }) } }, beforeDestroy() { if (this.mp) { this.mp.destroy() this.mp = null } } } </script> <style scoped> .player-wrap { width: 100%; height: 100vh; background: #000; } .mp-box { width: 100%; height: 100%; } </style>

几个细节值得说。container必须是一个已经渲染完成的 DOM 节点,所以初始化要放在$nextTick里,App 端尤其明显,onLoad阶段 DOM 还没挂上,直接初始化会拿到null。type字段告诉组件用哪个内核,'hls'对应 m3u8,'flv'对应 flv。live: true是直播模式的关键开关,它会影响进度条的显示方式和一些缓冲策略。

还有一点,父容器必须给明确高度。mui-player 内部是按容器的宽高去撑 video 的,如果父级是个高度为 auto 的 view,你会看到一片空白,然后怀疑是不是流有问题。我建议外层固定100vh或者固定像素高度,内层100%,最稳。

2.3 生命周期与实例管理这块最容易出事

单独开一节讲这个,因为它在实际项目里造成的 bug 数量远超播放本身。

uniapp 的页面生命周期和 Vue 的组件生命周期是两套东西。onHide(页面被切到后台或被新页面覆盖)不一定触发beforeDestroy,如果你只在一个地方写销毁,很容易出现“用户从直播页跳去详情页,回来之后声音有两份”的情况——因为旧实例还在后台拉着流。

我的处理方式是三个口子都堵上:onHide时暂停播放(mp.pause()),onUnload时销毁(mp.destroy()),beforeDestroy里再兜一次底,加个if (this.mp)判空。如果你用了keep-alive,activated和deactivated也要管起来,否则页面被缓存后实例一直活着,来回切换几次内存就上去了。

onHide() { this.mp && this.mp.pause() }, onUnload() { this.destroyPlayer() }, deactivated() { this.destroyPlayer() }, methods: { destroyPlayer() { if (this.mp) { this.mp.pause() this.mp.destroy() this.mp = null } } }

提示:destroy()不只是移除 DOM,它还会解绑内核实例、清掉事件监听、释放 MSE 的 SourceBuffer。如果你只把 DOM 移除了却留着内核实例,后台会持续拉流分片,用户流量就在你眼皮底下跑掉了。测试阶段可以用浏览器 DevTools 的 Network 面板观察页面切走后还有没有持续的 ts 请求,一试就知道。

3. m3u8 适配:把 HLS 这条链路吃透

m3u8 是整条链路里最需要“分端处理”的协议,不同设备的接活方式完全不同,参数调优也有讲究。

3.1 先判断谁该接活:原生播放还是 hls.js

HLS 的好处是有两套吃法。iOS 的 WebKit 和 PC 的 Safari 原生支持 HLS,你只要把 m3u8 地址塞进video.src,底层就自动解析播放列表、拉分片、切换码率,什么都不用做。其他环境(安卓、PC Chrome)不认识 m3u8,必须靠 hls.js 把分片拉下来,通过 MSE 喂给 video 元素。

所以判断逻辑要写成这样,顺序不能反:

const canNativeHls = !!video.canPlayType('application/vnd.apple.mpegurl') const canMse = window.MediaSource || window.WebKitMediaSource if (canNativeHls) { // 原生 HLS 路线,直接把 src 交给 video video.src = url } else if (canMse && Hls.isSupported()) { // hls.js 路线 const hls = new Hls(hlsConfig) hls.loadSource(url) hls.attachMedia(video) } else { // 两条路都不通,走降级提示 this.showFallback() }

这里有个很多人会写反的地方:先判断Hls.isSupported()再判断原生。结果在 iOS 上是能跑,但跑的是 hls.js 链路——iOS 上的 MSE 支持历来不完整,白白多了一层损耗,还容易触发一些奇怪的解码问题。正确的顺序永远是先问原生,原生不行再上 MSE。

Hls.isSupported()内部会检查 MSE 的 API 完整度,包括SourceBuffer.appendBuffer这些。有些低版本安卓 WebView 会漏掉几个方法,isSupported()返回 false,这时候就算你把流塞进去也是黑屏,不如早点走降级。

3.2 hls.js 参数怎么调,延迟和卡顿怎么平衡

默认配置下的 HLS 直播延迟通常在 15 到 30 秒,对监控、连麦这类场景是不能接受的。延迟的构成其实是可以算的:主要来自“播放点落后直播边缘的距离”,而这个距离由liveSyncDurationCount乘以分片时长决定。

假设服务端切片时长是 4 秒,liveSyncDurationCount默认是 3,那理论延迟下限就是 12 秒,再加上网络往返和播放器缓冲,实际测出来 15 秒左右很正常。想压到 8 秒以内,就要么把分片切到 2 秒,要么把liveSyncDurationCount改成 2。但这两个参数压得越狠,弱网下起播缓冲和卡顿的概率就越高,这是在拿稳定性换延迟,必须两边一起测。

我常用的折中配置大概是这样的:

const hlsConfig = { enableWorker: true, // 把解析放到 Worker,主线程不卡 lowLatencyMode: true, // 开启低延迟模式 liveSyncDurationCount: 3, // 落后直播边缘 3 个分片 liveMaxLatencyDurationCount: 6, maxBufferLength: 10, // 前向缓冲上限,秒 maxMaxBufferLength: 30, backBufferLength: 15, // 回看缓冲,直播场景可以砍小 manifestLoadingTimeOut: 10000, manifestLoadingMaxRetry: 3, manifestLoadingRetryDelay: 1000, fragLoadingMaxRetry: 6, fragLoadingRetryDelay: 800, fragLoadingTimeOut: 20000 }

backBufferLength这个参数值得单独说。它控制已经播过的内容在内存里留多久,默认值比较大。直播场景用户基本不会往回拖,把这个值压到 15 秒以内能明显降低内存占用,在低端安卓机上效果肉眼可见。enableWorker建议开,分片解析放在 Worker 里做,主线程不会因为解析 m3u8 列表而掉帧。

如果 mui-player 的版本不支持透传hlsConfig,别硬啃,直接走“自建内核”的路子:自己new Hls(config),然后通过容器选择器拿到 mui-player 内部的 video 元素,手动attachMedia。这套写法绕开了插件的版本兼容问题,我在两个项目里都用过,非常稳。

const mp = new MuiPlayer({ container: '#mp-container', autoplay: true, live: true }) const videoEl = document.querySelector('#mp-container video') const hls = new Hls(hlsConfig) hls.loadSource(url) hls.attachMedia(videoEl)

3.3 切码率、断流重连与首帧优化

清晰度切换在 mui-player 里靠switchUrl就够了,但我建议自己维护一个“当前清晰度”的状态,避免用户连点导致几个请求叠在一起。

switchQuality(item) { if (this.currentQuality === item.level) return this.currentQuality = item.level this.mp.switchUrl(item.url) }

切换的时候有两个体验细节:一是切换瞬间画面会黑一下,可以提前把 poster 或者上一帧截图垫上去;二是切换后播放进度会回到直播边缘,如果你的业务需要“切清晰度后从原来的时间点继续”,就得自己记住currentTime再 seek 回去,不过直播场景一般不需要。

断流重连是必须做的。移动网络在 4G 和 WiFi 之间切换时,连接会断,hls.js 会抛错误。我的处理是监听Hls.Events.ERROR,区分致命和非致命:网络类错误(NETWORK_ERROR)做延迟重试,媒体类错误(MEDIA_ERROR)先尝试recoverMediaError(),两次无效就整体重建内核实例。

hls.on(Hls.Events.ERROR, (event, data) => { if (!data.fatal) return if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { setTimeout(() => hls.startLoad(), 1500) } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError() } else { this.rebuildPlayer() } })

首帧优化方面,preload设成'auto',autoplay打开,同时把 poster 设成一张有意义的封面图(比如公司 logo 或者当前直播间的截图),这样从进入页面到出画面的那一两秒不会是一片纯黑,用户不会以为坏了。

4. flv 适配:低延迟的代价与兜底

flv 的价值就一个字:快。HTTP-FLV 走的是长连接持续推流,不需要切片,延迟通常能压到 1 到 3 秒,比 HLS 强一个数量级。但它对设备的要求也高得多,属于“能用就是惊喜,不能用也别意外”的那类方案。

4.1 flv.js 凭什么快,又凭什么挑设备

flv.js 的原理是把 FLV 流拉下来,在浏览器里做解封装,取出 H.264 的裸流和 AAC 音频,再通过 MSE 的SourceBuffer喂进去。整个链路绕开了浏览器内置的容器解析,延迟自然低。

代价是它完全依赖 MSE。这意味着:

  • iOS 上基本没戏。iOS 的 WebKit 对 MSE 的支持长期不完整,MediaSource对象存在但功能受限,flv.js 的isSupported()会返回 false。
  • 部分老安卓机、低版本 WebView 也不行,MSE 的 API 缺失或者SourceBuffer的 codec 支持不全。
  • PC 端的 Chrome、Edge、Firefox 没问题,Safari 要具体测,别想当然。

所以 flv 的定位必须明确:在支持的设备上用它换低延迟,在不支持的设备上无缝回落到 m3u8。这个回落不能是弹个提示让用户自己刷新,必须是代码里自动完成。

4.2 一个能跑的低延迟配置

const flvConfig = { isLive: true, hasAudio: true, hasVideo: true, enableStashBuffer: false, // 关掉缓存池,降低延迟 stashInitialSize: 128, lazyLoad: false, autoCleanupSourceBuffer: true, autoCleanupMaxBackwardDuration: 15, autoCleanupMinBackwardDuration: 10, fixAudioTimestampGap: false }

enableStashBuffer是核心。开着它的时候,flv.js 会先攒一段数据再开始播,起播更稳但延迟高;关掉之后数据来了就喂,延迟低但弱网下容易卡。我一般在 WiFi 环境关掉、在移动网络开着,用navigator.connection.effectiveType做个简单判断就够了,没必要做得太复杂。

autoCleanupSourceBuffer系列参数管的是内存回收。直播流是无限长的,如果不清理,SourceBuffer会一直涨,播两三个小时之后页面就卡死了。把autoCleanupMaxBackwardDuration设成 15 秒左右,让它自动丢掉老数据,这是长时间直播必须配的。

4.3 播不了的时候怎么降级

降级链路我设计成三级,写在一个方法里,逐级往下试。

第一级,判断flvjs.isSupported()且不是 iOS,用 flv 地址。 第二级,回落到同一路内容的 m3u8 地址,走 hls.js 或者原生 HLS。 第三级,如果连 m3u8 都没有(服务端只推了 flv),就展示一个带说明的占位页,文案写清楚“当前设备暂不支持该直播格式,建议使用电脑端观看”,别让用户对着黑屏猜。

chooseSource(streams) { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) const flvOK = !isIOS && window.MediaSource && flvjs.isSupported() if (flvOK && streams.flv) return { url: streams.flv, type: 'flv' } if (streams.m3u8) return { url: streams.m3u8, type: 'hls' } return null }

注意:降级判断里不要把 UA 当成唯一依据。有些安卓平板的 UA 里带着 iPad 字样,有些定制浏览器的 UA 被改得面目全非。UA 只做粗筛,真正的判定还是要落在isSupported()这类特征检测上。

5. 移动端与 PC 端的差异处理

前面两节讲的是协议层,这一节讲的是“同一套代码在不同端上的行为差异”。这部分内容在官方文档里几乎找不到,全是从真机调试里攒出来的。

5.1 端能力探测:别用 UA 猜,用特征判断

我在项目里维护了一个getDeviceCapability(),返回一个对象,包含:是否 iOS、是否安卓、是否在定制的 WebView 里、MSE 是否可用、是否支持原生 HLS、当前网络类型、屏幕方向。所有播放相关的分支判断都以这个对象为准,页面里不出现第二处 UA 判断。

原因很简单:UA 判断散落在十几个地方,改一次要改十几处,而且极容易漏。集中到一处之后,遇到新机型只需要改这一个函数。

function getDeviceCapability() { const ua = navigator.userAgent const isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) const isAndroid = /Android/i.test(ua) const video = document.createElement('video') return { isIOS, isAndroid, canNativeHls: !!video.canPlayType('application/vnd.apple.mpegurl'), canMse: !!(window.MediaSource || window.WebKitMediaSource), isMobile: isIOS || isAndroid, networkType: (navigator.connection && navigator.connection.effectiveType) || 'unknown' } }

那个maxTouchPoints > 1的判断是给 iPadOS 13 之后准备的——它默认请求桌面版网站,UA 跟 Mac 一模一样,只靠iPad关键词会判错,导致 iPad 走上 PC 分支。

5.2 iOS、安卓、定制内核 WebView 的三处硬骨头

第一处,内联播放。iOS 的 video 默认是全屏播放,一点就跳出去,这对嵌在页面里的播放器是灾难。必须给 video 元素加上playsinline和webkit-playsinline两个属性。mui-player 生成的 video 在大多数情况下会带,但我不敢赌,初始化之后手动补一遍:

const videoEl = document.querySelector('#mp-container video') videoEl.setAttribute('playsinline', 'true') videoEl.setAttribute('webkit-playsinline', 'true') videoEl.setAttribute('x5-playsinline', 'true')

第二处,安卓定制内核的全屏劫持。国内安卓端不少浏览器用的是定制内核,video 元素会被系统接管成一个原生层,盖在所有 DOM 之上,你写的弹窗、礼物特效、评论浮层全被压在下面。解决办法是加一组 X5 相关的属性,强制它走页面内渲染:

videoEl.setAttribute('x5-video-player-type', 'h5-page') videoEl.setAttribute('x5-video-player-fullscreen', 'false') videoEl.setAttribute('x5-video-orientation', 'portrait')

加上之后,video 就变回普通 DOM 元素,z-index 正常生效,浮层也能盖上去了。这个属性在非定制内核的浏览器里加了也没副作用,所以我一般是无条件全部加上,省得判断。

第三处,自动播放策略。移动端浏览器和 PC 端 Chrome 都限制了带声音的自动播放,play()会抛NotAllowedError。唯一的办法是先静音再播,播起来之后引导用户点一下开启声音。

videoEl.muted = true videoEl.play().then(() => { // 起播成功,显示“点击开启声音”的按钮 }).catch(err => { // 起播也被拦,显示遮罩让用户手动点 })

还有一种更彻底的方案,进入页面时放一个全屏的“点击进入直播间”遮罩,用户点了才初始化播放器。这个方案用户体验稍差(多一步),但成功率是 100%,在直播这种强实时场景里我个人更倾向这个,因为一旦起播失败,用户看到的是黑屏加一个转圈,会直接跑掉。

5.3 PC 端的交互与性能细节

PC 端的差异主要在交互习惯和性能上。

交互上,PC 用户习惯用鼠标悬停显示控制条、习惯用键盘的空格暂停、方向键调音量、F 键全屏。mui-player 的keyShortcut配置项可以打开键盘快捷键,记得开。鼠标悬停显示控制条的逻辑它内置了,但如果你的页面有自己的 hover 样式,注意别把控制条盖住了。

性能上,PC 端浏览器标签页在后台时会降频,定时器和requestAnimationFrame都被限流。如果你的页面里有“每 3 秒检查一次播放状态”的逻辑,切到后台后这个检查会变成几十秒一次,回来的时候状态可能已经错乱了。处理办法是监听visibilitychange,页面重新可见时主动做一次状态校准,而不是依赖轮询。

还有一个 PC 端特有的问题:多标签页同时播同一个流。用户开了三四个标签页看直播,带宽被吃满,每个都在卡。这个从技术侧没法强制限制,但可以在同一域下用localStorage或者BroadcastChannel做个简单的心跳,检测到重复打开时弹个提示“检测到已在其他标签页观看,本次播放已暂停”,体验会好很多。

6. 踩坑实录与排查速查表

这部分是我两年里攒下来的问题清单,按现象归类,遇到时按表查能省不少时间。

6.1 黑屏、卡首帧、无限 loading

黑屏但控制条正常、时间在走。大概率是音频轨或者视频轨解码失败。先看流的编码格式,H.264 High Profile 5.1 在某些老安卓机上解不了,要降到 Main Profile 4.0。这个问题从播放器侧无解,只能服务端转码。

卡在首帧不动,一直转圈。三种可能:一是 m3u8 索引文件拉到了但 ts 分片 404 或者跨域被拦;二是分片是 AES 加密的,客户端没传密钥;三是liveSyncDurationCount设得太大,播放点被推到了很远的位置。排查方法是直接在新标签页打开 m3u8 地址,看返回的列表里 ts 路径是不是相对路径、能不能直接访问。

iOS 上黑屏其他端正常。九成是playsinline没加,或者用 hls.js 走了 MSE 路线。前面说过,iOS 一定要走原生 HLS。

6.2 内存泄漏与后台偷跑流量

长时间直播页面最容易出的问题就是内存持续上涨。三个源头:SourceBuffer没回收、内核实例没销毁、事件监听没解绑。

SourceBuffer靠 hls.js 的backBufferLength和 flv.js 的autoCleanup参数控制。内核实例靠destroy()。事件监听这块最容易漏,尤其是你在页面里给 video 加了timeupdate、waiting这类回调,销毁时一定要removeEventListener,否则旧闭包一直挂在 video 元素上。

验证方法很土但有效:在开发者工具的性能面板里录一段五分钟的曲线,看 JS 堆大小是不是持续单调上升。如果是,那就是泄漏;如果涨到某个水位就平了,那是正常的 GC 波动。

后台偷跑流量的验证更简单,Network 面板不清空,把页面切走,看还有没有持续的请求往外发。有,就说明实例没销毁干净。

6.3 排查速查表

现象最可能的原因处理方向
全端黑屏流地址本身不可用、鉴权参数过期浏览器直开地址验证,检查 token 时效
仅 iOS 黑屏缺 playsinline、走了 MSE 路线补属性,改原生 HLS
仅安卓黑屏不支持 MSE、编码 Profile 过高检测 MSE,服务端降 Profile
首帧卡住ts 分片跨域、路径错误、加密无密钥直接访问分片地址,检查 CORS
播放几秒就断分片超时、网络抖动无重连加 ERROR 监听与重试
延迟十几秒HLS 切片过长、sync 段数过多调切片时长与 syncDuration
浮层被视频盖住定制内核原生层接管加 x5 相关属性强制内联
后台回来卡死实例未校准、缓冲失效监听 visibilitychange 重建
内存持续上涨实例未销毁、缓冲未回收补 destroy 与 autoCleanup
切页面后仍有声音旧实例未销毁onHide / onUnload 双保险

最后分享一个我调试时的习惯:在播放器实例外面挂一个全局可访问的调试对象,把内核实例、video 元素、能力对象、当前配置全挂上去。真机连上调试时,在控制台里能直接看到hls.bufferController这类内部状态,比加一堆 console 快得多。当然上线前记得去掉,或者在process.env.NODE_ENV === 'development'里包一层。

再补一个后续可以扩展的方向:如果你对延迟的要求压到 1 秒以内,HLS 和 FLV 都到极限了,那就得看 WebRTC 这条线了。但那是另一套东西,信令、ICE、NAT 穿透、服务端媒体服务器全都要重搭,不是换个播放器插件能解决的。我的建议是先把手上的 m3u8 / flv 这套稳定下来,把各端兜底逻辑做扎实,等业务真的卡在延迟上了,再立独立的项目去做 WebRTC,别在同一个版本里同时搞两件复杂的事,容易两头都做不好。

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

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

立即咨询