拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS H5 中 HLS 播放失败:hls.js 与原生分流方案

iOS H5 中 HLS 播放失败:hls.js 与原生分流方案

做过移动端 H5 视频的人,大概率都遇到过这种场面:Android 手机上 HLS 流播得好好的,一到 iOS 的 H5 里就黑屏、转圈、只有声音没画面,甚至控制台直接抛出MediaSource is not defined或者Hls is not supported。标题里说的 IOS H5 页面中 HLS 视频无法正常播放,使用 hls.插件,现实项目里这个“hls.插件”多半就是 hls.js。先把结论放前面:iOS 的 Safari 和 WKWebView 原生支持 HLS,但对 MSE 的支持长期缺失或受限,而 hls.js 的核心工作方式又依赖 MSE,所以无脑在 iOS 上new Hls(),失败概率非常高。正确做法是先做能力探测,iOS 走原生video.src = m3u8,Android 和桌面浏览器再交给 hls.js,最后把两套逻辑封装成同一个播放器接口。这个思路适合前端、H5、混合 App、微信内嵌页、uni-app 以及小程序 web-view 场景下的开发者参考,不管你是刚接触 HLS,还是已经被 iOS 的播放问题折腾过几轮,都可以按下面的步骤逐项排查和落地。

1. 先把问题说透:iOS H5 里 HLS 播放失败到底长什么样

1.1 现象不是一种,而是三类

第一类是“完全不能播”。页面加载后 video 区域一直黑,控制台报Hls is not supported、MediaSource is not defined、NotSupportedError,或者 hls.js 初始化阶段就失败。这种情况通常是代码没有判断环境,直接把 hls.js 当成全平台方案用了。第二类是“能播但体验不对”。视频能出画面,但自动播放失败、全屏异常、没有进度条、拖动后卡死、清晰度切换无效。第三类是“表面正常,偶发崩溃”。比如连续播放几个视频后页面卡顿,或者切到后台再回来播放器直接黑屏。这三类问题的根源不完全一样,但都绕不开 iOS 对媒体播放的限制。

很多人一看到黑屏就怀疑 m3u8 地址错了,其实在 iOS H5 里,地址正确但播放器选错,同样会黑屏。因为 iOS 的 video 元素可以原生解析 HLS,但你用 hls.js 把流拆成 MP4 分片再喂给 video,就需要 MSE 支持。iOS 的 WebView 如果没开 MSE,或者只支持有限的 Managed Media Source,hls.js 就接不上。这个区别非常关键,也是后面所有方案分流的基础。

1.2 为什么标题里特别强调“使用 hls.插件”

hls.js 的优势很明显:体积可控、API 丰富、支持 ABR 自动码率、错误恢复、直播低延迟、清晰度手动切换,在 Android、Chrome、Firefox、Edge 以及桌面端都很好用。很多项目一开始只在 Android 和 PC 上验证,发现 hls.js 跑得通,就默认 iOS 也能跑。结果上线后 iOS 用户反馈黑屏,开发同学再回头查,才发现 iOS 根本不支持标准 MSE。这不是 hls.js 的 bug,而是平台能力差异。

所以标题里的“使用 hls.插件”其实点中了要害:不是 HLS 有问题,也不是 m3u8 一定有问题,而是“用 hls.js 播放 HLS”这个组合在 iOS 上不成立。你需要把这个组合拆开,按平台选择播放路径。iOS 用原生 HLS,Android 和桌面用 hls.js,其他环境再按容器能力判断。只有这样,才能既保留 hls.js 的灵活性,又避开 iOS 的限制。

1.3 解决路线一句话:能力探测后分流

我会把播放逻辑分成三层。第一层探测video.canPlayType('application/vnd.apple.mpegurl'),如果返回maybe或probably,优先走原生 HLS。第二层探测Hls.isSupported(),如果支持,走 hls.js。第三层做兜底提示,告诉用户当前环境不支持,或者引导升级系统、切换浏览器、使用 App 原生播放器。这个顺序很重要,不要反过来。因为有些 iOS 版本既支持原生 HLS,也可能支持有限的 Managed Media Source,但原生路径更稳,资源占用也更低。

很多团队会问:那我能不能在 iOS 上也强上 hls.js,只为了统一 API?理论上 iOS 17.1 之后部分 Safari 支持 Managed Media Source,hls.js 新版本可以尝试,但兼容性、性能、后台恢复、全屏行为都不如原生。线上项目要的是稳定,不是代码好看。所以我的建议很明确:iOS 优先原生,hls.js 作为非 iOS 环境的主力,封装层统一对外暴露play、pause、destroy、switchQuality等方法。

2. 原理拆解:hls.js、MSE 与 iOS 原生 HLS 的三角关系

2.1 HLS 本身是什么

HLS 全称 HTTP Live Streaming,核心思路是把一个完整视频切成很多小分片,再用一个索引文件把这些分片串起来。索引文件通常是.m3u8,里面记录了分片地址、时长、码率、版本等信息。播放器先下载 m3u8,再按顺序下载分片,边下边播。它的好处是适配 HTTP 基础设施,能过 CDN、能缓存、能根据网络切换码率,所以在直播和点播里都很常见。

iOS 对 HLS 的支持是系统级的。Safari、WKWebView、原生 AVPlayer 都能直接识别 m3u8。你把 m3u8 地址赋给 video 元素的src,系统媒体引擎会自己完成索引解析、分片下载、解码和播放。这个过程不需要页面 JS 参与,也不依赖 MSE。也就是说,在 iOS H5 里播放 HLS 最稳的方式,其实就是让 video 自己去做。

2.2 hls.js 的工作方式完全不同

hls.js 是一个 JavaScript 库,它把 HLS 的解析、分片下载、缓冲控制、码率切换都放在 JS 层完成。它拿到 m3u8 后,自己解析出分片列表,下载 TS 或 fMP4 分片,然后通过 MSE 把分片喂给 video 元素。MSE 提供的是MediaSource和SourceBuffer,相当于让 JS 可以动态向 video 里追加媒体数据。Android Chrome、桌面 Chrome、Firefox、Edge 都支持 MSE,所以 hls.js 在这些环境里工作得很好。

问题在于,iOS Safari 长期不支持标准 MSE。没有 MSE,MediaSource就不存在,hls.js 自然无法把分片塞进 video。你在 iOS 上看到MediaSource is not defined,不是网络问题,也不是 m3u8 问题,而是浏览器能力问题。即便某些版本提供了 Managed Media Source,也有额外限制,比如需要特定 API、对后台和全屏有约束,不能简单等同于 Android 上的 MSE。

2.3 iOS 原生 HLS 与 hls.js 的职责边界

在 iOS 上,原生 HLS 负责从 m3u8 到画面的一切。你不需要手动下载分片,也不需要自己管理缓冲。你只需要给 video 正确的 m3u8 地址,并处理好自动播放、内联播放、全屏和用户手势。hls.js 在 iOS 上最多只能做降级尝试,不能作为主路径。反过来,在 Android 和桌面浏览器上,很多环境没有原生 HLS 支持,尤其 Chrome 桌面版,必须靠 hls.js 或类似库。

所以职责边界很清楚:iOS 把 HLS 交给系统,其他环境把 HLS 交给 hls.js。封装层要做的是判断当前环境,然后选择对应实现。不要试图用一套 hls.js 代码打天下,那是踩坑的开始。

2.4 iOS 17.1 之后的 Managed Media Source 变化

iOS 17.1 之后,Safari 开始支持 Managed Media Source,这给 hls.js 在 iOS 上运行带来了一点可能性。但要注意,这不是全面放开。Managed Media Source 对使用方式、播放器状态、后台行为都有额外要求,而且旧版本 iOS 仍然不支持。线上用户不可能全部升级到最新系统,所以你依然不能把 hls.js 作为 iOS 的默认方案。

我的做法是:在能力探测里仍然优先判断原生 HLS,只有原生不可用时,才考虑ManagedMediaSource或MediaSource。如果都没有,就提示不支持。这样即使未来 iOS 支持得更好,也不会影响现有稳定路径。技术选型要向前兼容,但更要向后兼容。

2.5 分片链接是 .png 时到底看什么

有些 m3u8 里的分片地址以.png结尾,这不代表它真的是图片。分片文件实际是什么类型,要看服务端返回的Content-Type和字节内容。播放器通常根据 m3u8 里的声明、响应头和实际数据来判断,扩展名不是唯一依据。但如果服务端把.png分片返回成image/png,某些播放器或中间层可能会拒绝,导致加载失败。所以排查时要打开 Network 面板,看分片请求的响应头是不是正确的媒体类型。如果是合法授权的视频分发,建议统一返回video/mp2t或application/octet-stream,避免 MIME 误判。

3. 环境判断与播放器选型:别一上来就 new Hls

3.1 三段式能力探测代码

播放器初始化前,先做能力探测。下面这段代码可以直接用:

function getPlaybackMode(video) { const canNativeHls = video.canPlayType('application/vnd.apple.mpegurl'); if (canNativeHls === 'maybe' || canNativeHls === 'probably') { return 'native-hls'; } const canMSE = 'MediaSource' in window || 'ManagedMediaSource' in window; if (canMSE && window.Hls && Hls.isSupported()) { return 'hlsjs'; } return 'unsupported'; }

这段逻辑的顺序很关键。先判断原生 HLS,再判断 MSE 和 hls.js。iOS Safari 通常会返回maybe,于是直接走原生。Android Chrome 原生 HLS 返回空字符串,但Hls.isSupported()为 true,于是走 hls.js。桌面 Chrome 同理。如果都不支持,就进入兜底提示。注意不要只判断Hls.isSupported(),因为 iOS 上它可能返回 false,也可能因为缺少 MSE 而在后续步骤失败。

3.2 不同容器里的差异:Safari、WKWebView、微信、uni-app

Safari 浏览器里,原生 HLS 支持最好,自动播放策略也相对明确。WKWebView 里,App 需要配置allowsInlineMediaPlayback、mediaTypesRequiringUserActionForPlayback等参数,否则视频可能全屏播放或者无法内联。微信 iOS 内置浏览器基于 WKWebView,整体支持 HLS,但视频可能被微信接管,出现层级、全屏、返回后黑屏等问题。uni-app 的 video 组件在 App 端可能走原生播放器,在 H5 端则回到浏览器能力,需要区分编译平台。

小程序 web-view 里更特殊。它本质上还是 WebView,但受小程序平台限制,视频域名、业务域名、同层渲染、全屏行为都要按平台规则处理。不要假设在 Safari 能播,在 web-view 里就一定能播。最稳的验证方式是真机跑一遍,而不是只看模拟器或开发者工具。

3.3 选型建议表

环境原生 HLSMSE / hls.js推荐方案
iOS Safari支持旧版不支持,新版有限原生 video + m3u8
iOS WKWebView支持取决于配置原生 video + m3u8
微信 iOS支持不推荐依赖原生 video + 用户手势
Android Chrome多数不支持支持hls.js
桌面 Chrome不支持支持hls.js
桌面 Safari支持有限原生优先
uni-app App原生播放器视端而定按平台条件编译
小程序 web-view受平台限制不稳定平台组件或原生能力

这张表不是绝对,因为系统版本和浏览器版本一直在变。但大方向不会错:iOS 优先原生,Android 和桌面优先 hls.js。

4. 实操:封装一个 iOS/Android 通用的 HLS 播放器

4.1 最小可用页面结构

先把 HTML 骨架搭好。video 标签要加上playsinline、webkit-playsinline,Android 微信里还可以加x5-playsinline、x5-video-player-type="h5"。这些属性不是装饰,它们直接影响视频是否内联播放、是否被浏览器接管全屏。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <title>HLS 播放测试</title> <style> html, body { margin: 0; padding: 0; background: #000; } .player-wrap { position: relative; width: 100vw; height: 56.25vw; max-height: 70vh; background: #000; } video { width: 100%; height: 100%; object-fit: contain; background: #000; } </style> </head> <body> <div class="player-wrap"> <video id="player" controls playsinline webkit-playsinline x5-playsinline x5-video-player-type="h5" x5-video-player-fullscreen="false" preload="metadata" ></video> </div> <script src="./hls.min.js"></script> <script src="./player.js"></script> </body> </html>

注意,hls.js 建议下载到本地静态资源里,不要每次运行时去外网拉。外网 CDN 在某些网络环境下可能不稳定,也会增加首屏等待时间。把hls.min.js放进项目静态目录,用相对路径引入,线上更可控。

4.2 核心播放逻辑与代码

下面是封装后的核心逻辑。它先探测环境,再选择原生还是 hls.js。对外暴露统一对象,方便业务层调用。

function createHlsPlayer(videoEl, url) { let hls = null; let mode = 'unknown'; const canNativeHls = videoEl.canPlayType('application/vnd.apple.mpegurl'); if (canNativeHls === 'maybe' || canNativeHls === 'probably') { mode = 'native'; videoEl.src = url; videoEl.addEventListener('loadedmetadata', () => { videoEl.play().catch((err) => { console.warn('原生播放失败,可能需要用户手势', err); }); }); return { mode, play() { return videoEl.play(); }, pause() { videoEl.pause(); }, destroy() { videoEl.pause(); videoEl.removeAttribute('src'); videoEl.load(); } }; } if (window.Hls && Hls.isSupported()) { mode = 'hlsjs'; hls = new Hls({ enableWorker: true, lowLatencyMode: false, backBufferLength: 30, maxBufferLength: 30, maxMaxBufferLength: 60, manifestLoadingTimeOut: 10000, manifestLoadingMaxRetry: 3, levelLoadingTimeOut: 10000, levelLoadingMaxRetry: 3, fragLoadingTimeOut: 20000, fragLoadingMaxRetry: 6, startLevel: -1, capLevelToPlayerSize: true }); hls.loadSource(url); hls.attachMedia(videoEl); hls.on(Hls.Events.MANIFEST_PARSED, () => { videoEl.play().catch((err) => { console.warn('hls.js 播放失败,可能需要用户手势', err); }); }); hls.on(Hls.Events.ERROR, (event, data) => { console.error('hls.js 错误', data); if (!data.fatal) return; if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { hls.startLoad(); } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError(); } else { hls.destroy(); } }); return { mode, play() { return videoEl.play(); }, pause() { videoEl.pause(); }, destroy() { if (hls) { hls.destroy(); hls = null; } videoEl.removeAttribute('src'); videoEl.load(); } }; } throw new Error('当前环境不支持 HLS 播放'); }

这段代码的关键点有三个。第一,原生路径和 hls.js 路径分开,互不干扰。第二,hls.js 的错误恢复按类型处理,网络错误重试加载,媒体错误尝试恢复,致命错误销毁。第三,销毁时一定要调用hls.destroy(),否则页面切来切去会残留缓冲和监听,时间长了容易卡顿。

4.3 hls.js 参数怎么调

enableWorker: true可以让解析工作放到 Web Worker,减少主线程压力,但部分老设备可能不稳定,如果发现播放异常可以关掉。lowLatencyMode适合直播低延迟场景,点播不要开。backBufferLength控制回撤缓冲,移动端内存小,设置 30 秒左右比较合适。maxBufferLength和maxMaxBufferLength控制前向缓冲,太大占内存,太小容易卡顿。startLevel: -1表示自动选择起始码率,capLevelToPlayerSize: true可以避免小窗口加载过高码率。

这些参数不是越大越好。移动端内存有限,缓冲太多可能导致页面崩溃,尤其在 iOS 上,WebView 内存超限会被系统直接回收。我的经验是:点播场景下maxBufferLength控制在 30 到 60 秒,直播场景再按延迟要求压缩。如果用户经常拖动进度条,可以适当增大backBufferLength,但不要无限制增长。

4.4 自动播放与内联播放的坑

iOS 对自动播放限制很严。一般情况下,必须由用户手势触发play(),或者视频处于静音状态。很多项目希望进入页面就自动播放,这在 iOS 上很难稳定实现。更稳的做法是放一个封面图,用户点击后再播放。如果是信息流场景,可以尝试muted加playsinline,但仍然要准备好被拦截的情况。

内联播放也很重要。如果不加playsinline和webkit-playsinline,iOS 可能会把视频强制全屏,页面布局和交互都会乱。App 内嵌 WKWebView 时,还要在原生侧设置allowsInlineMediaPlayback = true,否则前端加再多属性也没用。微信内还要注意视频层级,有时候视频会盖住弹窗,需要做同层渲染或隐藏处理。

4.5 播放器销毁与页面生命周期

单页应用里,路由切换时一定要销毁播放器。否则 video 还在后台加载,hls.js 还在请求分片,内存和带宽都会被浪费。监听visibilitychange,页面隐藏时暂停,页面恢复时再按需继续。不要小看这个细节,很多“播放几个视频后页面卡死”的问题,都是因为旧播放器没有销毁。

document.addEventListener('visibilitychange', () => { if (document.hidden) { player.pause(); } }); window.addEventListener('beforeunload', () => { player.destroy(); });

如果是列表页多个视频,建议只保留当前可见视频的播放器实例,其他全部销毁。移动端不要同时创建多个 hls.js 实例,否则主线程和内存都扛不住。

5. 服务端与 CDN 侧:别让配置把播放器坑死

5.1 MIME、CORS、Range 三件套

播放 HLS 时,服务端有三个配置必须检查。第一是 MIME。m3u8 最好返回application/vnd.apple.mpegurl或application/x-mpegURL,TS 分片返回video/mp2t,fMP4 分片返回video/mp4。如果分片是.png后缀,也要返回正确的媒体类型或application/octet-stream,不要返回image/png。第二是 CORS。原生 video 播放 m3u8 对页面 CORS 要求相对宽松,但 hls.js 通过 XHR 或 fetch 加载分片,必须有Access-Control-Allow-Origin。第三是 Range。拖动进度条时,播放器可能发 Range 请求,服务端要支持206 Partial Content,否则拖动会失败或者从头下载。

排查时可以直接用命令行看响应头:

curl -I https://example.com/video/index.m3u8 curl -I https://example.com/video/seg_001.ts

重点看Content-Type、Access-Control-Allow-Origin、Accept-Ranges、Content-Range。如果这些不对,播放器代码写得再好也没用。

5.2 m3u8 与分片路径

m3u8 里的分片地址可以是相对路径,也可以是绝对路径。相对路径会基于 m3u8 所在目录解析,如果 CDN 做了路径重写,容易 404。建议生成 m3u8 时使用清晰的相对路径,并在 CDN 侧保持目录结构一致。如果分片链接后缀是.png,要确认 CDN 没有把它当成图片做特殊处理,比如图片压缩、格式转换、缓存策略覆盖。任何对媒体分片的二次加工都可能导致字节流变化,播放器解析失败。

另外,m3u8 本身不要强缓存,否则直播流更新后客户端还在用旧索引。TS 分片因为文件名通常带序号或哈希,可以设置较长缓存。这样既保证索引实时性,又减少分片回源。

5.3 缓存策略与 HTTPS

HTTPS 页面里加载 HTTP 视频会被浏览器拦截,这是混合内容限制。所有 m3u8 和分片地址都必须是 HTTPS。App 内嵌 WKWebView 还要检查 ATS 配置,合法域名需要加入例外或使用合规证书。不要为了让视频能播就关闭全局 ATS,那样会带来安全风险,也可能影响上架审核。

缓存策略上,m3u8 可以设置Cache-Control: no-cache或较短时间,分片设置Cache-Control: public, max-age=31536000, immutable。如果 CDN 支持,开启分片缓存和 Range 回源,能显著改善拖动和二次播放体验。

6. 常见问题与排查技巧实录

6.1 典型故障速查表

现象可能原因排查方式处理建议
iOS 黑屏,Android 正常hls.js 依赖 MSE,iOS 不支持看是否MediaSource is not definediOS 改走原生 video.src
一直转圈自动播放被拦截控制台看 NotAllowedError用户点击后再 play
manifestLoadErrorm3u8 请求失败或 CORSNetwork 看状态码和响应头配置 CORS、检查地址
分片 404相对路径或 CDN 重写看 m3u8 内的分片路径修正路径或回源规则
只有第一帧编码或 MIME 不对看分片 Content-Type转 H.264 + AAC,修正 MIME
拖动失败服务端不支持 Rangecurl 看 Accept-Ranges开启 206 Partial Content
.png 分片不播返回 image/png看响应头返回 video/mp2t 或 octet-stream
微信内全屏异常微信接管视频真机观察加 playsinline、x5 属性
页面 HTTPS 视频 HTTP混合内容控制台安全提示全站 HTTPS
播放几轮后卡死播放器未销毁内存面板路由切换 destroy

6.2 控制台与 Network 怎么看

先看 Console。NotSupportedError、MediaSource is not defined、Hls is not supported基本都指向环境不支持。NotAllowedError是自动播放被拦截。manifestLoadError通常是 m3u8 请求失败、跨域或者 404。再看 Network,过滤 m3u8 和分片请求,检查状态码、响应头、响应大小。如果 m3u8 返回 200 但内容不对,可能是服务端返回了 HTML 错误页。如果分片请求 403,可能是鉴权参数过期。如果分片请求一直 pending,可能是网络或 CDN 问题。

真机调试时,iOS Safari 可以通过开发菜单连接电脑查看控制台。Android 可以用 Chrome Inspect。微信内可以打开调试模式,或者用 vConsole 之类的工具看日志。不要只在桌面浏览器模拟移动端,桌面和真机的媒体能力差异很大。

6.3 错误恢复代码模板

hls.js 的错误恢复不能只写一个console.log。网络错误可以重试,媒体错误可以恢复,但如果是 manifest 解析失败或者环境不支持,就要走降级。下面这个模板可以直接用:

hls.on(Hls.Events.ERROR, (event, data) => { if (!data.fatal) { console.warn('非致命错误', data.type, data.details); return; } switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: console.warn('网络错误,尝试重新加载'); hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: console.warn('媒体错误,尝试恢复'); hls.recoverMediaError(); break; default: console.error('致命错误,销毁播放器'); hls.destroy(); break; } });

如果是原生路径,没有 hls.js 错误事件,但可以监听 video 的error事件,根据video.error.code判断。常见的有MEDIA_ERR_SRC_NOT_SUPPORTED、MEDIA_ERR_NETWORK。原生路径下如果 m3u8 地址过期,也会触发错误,需要业务层重新获取地址。

6.4 真机调试经验

模拟器不能替代真机。iOS 模拟器的媒体能力和真机有差异,尤其是硬件解码、内存回收、后台播放。微信、钉钉、企业微信等容器也要分别测试。测试时至少覆盖:iOS Safari、iOS 微信、iOS App 内嵌 WKWebView、Android Chrome、Android 微信、桌面 Chrome、桌面 Safari。每个环境都验证首帧、播放、暂停、拖动、全屏、切后台、返回、销毁。只有这个矩阵跑完,才敢说方案稳。

7. 性能与体验优化:能播之后还要好用

7.1 首帧速度

首帧速度受 m3u8 长度、分片大小、起始码率、网络环境影响。m3u8 如果包含几百个分片,解析会变慢,建议点播用 VOD 清单,直播用滚动窗口。起始码率不要一上来就选最高,startLevel: -1让 hls.js 自动选,或者根据屏幕尺寸限制最高码率。分片时长建议 4 到 6 秒,太短请求多,太长首帧慢。原生 iOS 路径下,首帧主要看 CDN 和 m3u8 响应速度。

还可以做预加载。进入详情页前先请求 m3u8,或者用preload="metadata"让浏览器提前拿元数据。但不要过度预加载,移动端流量和内存都要考虑。

7.2 清晰度切换与卡顿

hls.js 支持手动切换清晰度,调用hls.currentLevel = levelIndex或hls.nextLevel。iOS 原生路径下,清晰度切换依赖系统 ABR,不能像 hls.js 那样直接控制。如果业务必须手动切清晰度,iOS 上可以准备多个不同码率的 m3u8,切换时重新设置 video.src。这样会重新加载,但兼容性最好。

卡顿通常来自缓冲不足、码率过高、网络抖动。可以开启capLevelToPlayerSize,小窗口不加载高码率。直播场景可以调整liveSyncDurationCount,但不要为了低延迟牺牲稳定性。移动端网络切换频繁,4G 和 Wi-Fi 之间切换时,hls.js 可能会报网络错误,错误恢复逻辑要能接住。

7.3 内存与后台恢复

iOS WebView 对内存很敏感,视频播放器占用内存较大。长时间播放或频繁切换视频,容易触发系统回收。要控制缓冲长度,及时销毁不用的播放器。切后台时暂停播放,切回来时重新检查播放状态。有些 iOS 版本切后台再回来,video 会黑屏,需要重新设置 src 或调用load()。这个行为没有统一标准,最好在真机上验证,并在业务层加一个“恢复播放”按钮或提示。

8. 我踩过的坑和给同行的建议

8.1 不要忽略用户手势

iOS 上自动播放失败是最常见的坑。很多人以为加了muted就能自动播,实际上不同版本、不同容器策略不一样。最稳的方式是让用户点击一次,再调用play()。如果是信息流,可以用封面图诱导点击,不要和系统策略硬碰硬。被拦截时不要只打日志,要给出可点击的播放按钮。

8.2 不要迷信一个插件

hls.js 很强,但不是万能。iOS 原生 HLS 更稳,Android 上 hls.js 更灵活。播放器封装要做能力探测,而不是绑定某一个库。未来如果 iOS 对 MSE 支持变好,你也可以在探测逻辑里逐步放开 hls.js,但不要一上来就全量切换,线上稳定比技术尝鲜重要。

8.3 测试矩阵要提前定

不要等上线后再发现 iOS 播不了。开发阶段就定好测试矩阵:系统版本、浏览器、容器、网络环境、视频类型。至少覆盖 iOS 15、16、17 和 Android 主流版本。每次修改播放器逻辑,都跑一遍核心用例。尤其是 m3u8 地址、分片 MIME、CORS、Range,这些服务端配置问题最容易在真机上暴露。

8.4 最后再分享一个小技巧

如果你在 iOS 上必须用 hls.js 做降级,可以先判断ManagedMediaSource,再判断MediaSource,并且把Hls.isSupported()放在后面。同时给 video 加上playsinline和webkit-playsinline,在 App 内嵌页面里让原生同学确认allowsInlineMediaPlayback已打开。排查问题时,先看 Console,再看 Network,最后看服务端响应头。只要把“原生优先、hls.js 兜底、服务端配置正确”这三件事做好,iOS H5 里的 HLS 播放问题基本就能从黑屏转圈变成稳定可用的状态。

返回列表