1. 流媒体适配的底层逻辑与方案选型做过流媒体播放的朋友大概率都经历过这种场景后端转码服务跑得好好的日志里切片文件一个不少m3u8 索引也正常生成可前端一打开页面要么黑屏转圈要么直接报一个MEDIA_ERR_SRC_NOT_SUPPORTED控制台里连个像样的错误堆栈都不给。折腾半天发现问题既不在网络也不在服务器而是卡在了浏览器对编码格式的支持差异上。这就是 HLS 视频编码兼容性最典型的坑也是我写这篇实战总结的直接原因。HLS全称 HTTP Live Streaming本质上就是把一整段视频切成一个个小的 TS 或 fMP4 分片再用一个 m3u8 文本索引把这些分片串起来播放器按顺序拉取、解码、渲染。它的核心优势是走标准 HTTP 协议天然穿透各类网络环境还能根据带宽动态切换不同码率的流。但它的“阿喀琉斯之踵”也很明显编码格式的兼容性完全取决于播放端浏览器或原生播放器的解码能力。服务端只管切能不能播是客户端的事。这篇文章面向的是正在做流媒体播放适配的开发者和运维同学尤其是那些后端用 FFmpeg 转码、前端用 hls.js 或原生 video 标签、移动端用 ExoPlayer 或 Media3 的团队。我会把 H.264 和 H.265 在不同浏览器、不同平台上的支持现状讲透把常见的排错路径一条条拆开再给出可以直接抄作业的配置方案。不管你是刚接触流媒体的新手还是已经踩过几轮坑的老手应该都能从里面找到对你有用的东西。1.1 为什么编码格式是 HLS 适配的第一道坎要理解兼容性问题得先搞清楚 HLS 播放链路里编码格式到底在哪些环节起作用。一条完整的链路是这样的采集端拿到原始视频流转码服务把它编码成 H.264 或 H.265封装成 TS 或 fMP4 分片CDN 分发出去播放器拉取分片后交给底层解码器解码最后渲染到屏幕上。这里面编码格式决定了分片里的数据长什么样而解码器决定了播放端能不能读懂这些数据。H.264也叫 AVC是 2003 年定稿的老牌标准经过二十多年发展几乎所有能播视频的设备都支持硬解。它的压缩效率放在今天看一般但胜在兼容性无敌。H.265也叫 HEVC是 2013 年推出的下一代标准同样画质下码率能比 H.264 低 40% 到 50%对带宽敏感的场景非常友好。但问题在于H.265 的专利授权体系极其复杂导致很多浏览器厂商在支持上非常保守尤其是涉及软件解码的部分。这就引出了一个关键认知H.265 在浏览器上的支持几乎完全依赖硬件解码能力。Chrome 在 Windows 上能不能播 H.265取决于你的显卡和驱动是否支持 HEVC 硬解以及操作系统有没有装对应的解码组件。同一台机器Edge 能播Chrome 可能就播不了因为两者的解码策略不同。这种碎片化的支持现状就是适配工作最大的难点。1.2 主流浏览器对 H.264 与 H.265 的支持现状我把目前主流浏览器和平台的支持情况整理成了一张表这张表是基于我实际测试和公开资料交叉验证的结果可以作为你选型时的参考基线。浏览器/平台H.264 (AVC)H.265 (HEVC)备注Chrome (Windows)完全支持依赖硬件解码需显卡和系统支持 HEVCChrome (macOS)完全支持支持系统级硬解macOS 原生支持 HEVCChrome (Android)完全支持部分支持取决于设备芯片Edge (Windows)完全支持支持含系统解码组件Windows 平台支持最好Safari (macOS/iOS)完全支持完全支持苹果生态原生支持Firefox完全支持基本不支持软件解码未开放移动端 ExoPlayer/Media3完全支持依赖设备硬解Android 5.0 需判断能力从这张表能看出几个规律。第一H.264 是真正的通用语言没有任何一个主流平台会拒绝它。第二H.265 的支持呈现明显的生态割裂苹果全家桶最积极Windows 上的 Edge 表现最好Chrome 看硬件脸色Firefox 基本躺平。第三移动端 Android 的 H.265 支持完全取决于设备芯片旗舰机基本没问题中低端机就要打问号了。提示不要用“浏览器版本号”来判断 H.265 支持同一个 Chrome 版本在不同硬件上表现可能完全不同。判断依据应该是运行时的能力检测而不是静态的版本对照表。1.3 方案选型的核心权衡兼容性、带宽与成本知道了支持现状接下来就是选型。这里没有标准答案只有权衡。我一般会从三个维度来考虑兼容性覆盖范围、带宽成本、转码成本。如果你的用户群体非常分散什么设备都有那 H.264 是唯一稳妥的选择。它的兼容性接近 100%你不用担心用户打不开。代价是带宽成本高同样画质下比 H.265 多消耗 40% 左右的流量。对于日活百万级的应用这个差价可能非常可观。如果你的用户主要集中在苹果生态或者你能确定目标设备都支持 H.265 硬解那用 H.265 能省下大量带宽。但你要做好降级方案一旦检测到不支持立刻切回 H.264 流。最务实的做法是双编码并行服务端同时转出 H.264 和 H.265 两套流m3u8 索引里通过CODECS属性标注编码格式播放端根据自身能力选择。这样既保证了兼容性又能在支持的设备上省带宽。代价是转码和存储成本翻倍需要根据业务规模算账。我在实际项目里更倾向于“H.264 为主H.265 为辅”的策略默认走 H.264检测到设备支持 H.265 且网络条件一般时再切换到 H.265 流。这样既不会因为兼容性问题丢用户又能在合适场景下优化体验。2. HLS 编码参数与分片封装的实操细节选型定了接下来就是具体的编码和封装。这一步的细节非常多一个参数没配对可能就会导致播放端解析失败。我见过太多案例视频在本地播放器里好好的一放到浏览器就出问题根源往往就在编码参数或封装格式上。2.1 H.264 编码的关键参数配置用 FFmpeg 做 H.264 转码有几个参数是必须关注的。我先给一份经过实战验证的基础配置然后再逐条解释为什么这么设。ffmpeg -i input.mp4 \ -c:v libx264 \ -profile:v high \ -level:v 4.1 \ -pix_fmt yuv420p \ -preset medium \ -crf 23 \ -g 48 \ -keyint_min 48 \ -sc_threshold 0 \ -c:a aac \ -b:a 128k \ -ar 44100 \ -ac 2 \ -f hls \ -hls_time 6 \ -hls_playlist_type vod \ -hls_segment_filename segment_%03d.ts \ output.m3u8-profile:v high和-level:v 4.1这两个参数决定了编码的“档次”。High Profile 提供了更好的压缩效率Level 4.1 则限制了最大码率和分辨率确保大多数设备能解码。如果你的目标设备比较老可以降到 Main Profile 和 Level 3.1兼容性会更好但压缩效率会下降。-pix_fmt yuv420p是必须设置的。浏览器对像素格式非常挑剔如果输出的是 yuv444p 或 yuv422p很多浏览器直接拒绝播放。yuv420p 是兼容性最好的选择虽然色彩信息有损失但肉眼基本看不出来。-g 48和-keyint_min 48控制关键帧间隔。HLS 分片必须从关键帧开始所以关键帧间隔要和分片时长匹配。假设视频是 24fps分片时长 2 秒那关键帧间隔就应该是 48。如果这个值设得不对FFmpeg 会在切片时强制插入关键帧导致码率波动和画质下降。-sc_threshold 0是关闭场景切换检测。默认情况下FFmpeg 会在场景切换时插入关键帧这会打乱我们设定的关键帧间隔导致分片边界不规整。关掉它关键帧就会严格按照-g的设定出现。2.2 H.265 编码的差异化配置与注意事项H.265 的配置思路和 H.264 类似但有几个关键差异。首先是编码器选择libx265 的编码速度比 libx264 慢很多如果转码量大需要考虑硬件加速方案。其次是 Profile 的选择H.265 的 Main Profile 对应 H.264 的 High ProfileMain 10 则支持 10bit 色深。ffmpeg -i input.mp4 \ -c:v libx265 \ -profile:v main \ -pix_fmt yuv420p \ -preset medium \ -crf 28 \ -g 48 \ -keyint_min 48 \ -sc_threshold 0 \ -tag:v hvc1 \ -c:a aac \ -b:a 128k \ -f hls \ -hls_time 6 \ -hls_playlist_type vod \ -hls_segment_filename segment_%03d.ts \ output.m3u8这里重点说-tag:v hvc1这个参数。H.265 在 MP4 容器里有两个常见的 taghvc1和hev1。苹果系的设备Safari、iOS、macOS只认 hvc1如果 tag 是 hev1Safari 会直接拒绝播放。这个坑我踩过不止一次明明编码没问题就是播不了最后发现是 tag 的问题。所以只要你的目标平台包含苹果设备这个参数必须加上。另外H.265 的 CRF 值一般比 H.264 高 4 到 6因为它的压缩效率更高。H.264 用 CRF 23 的话H.265 用 CRF 28 左右能获得相近的画质。但这不是绝对的具体还要看内容复杂度。2.3 TS 与 fMP4 分片格式的选择依据HLS 的分片格式主要有两种传统的 MPEG-TS 和较新的 fMP4碎片化 MP4。两者各有优劣选择哪个要看你的具体场景。TS 格式的优点是兼容性极好几乎所有支持 HLS 的播放器都能播包括各种老旧的机顶盒和智能电视。缺点是封装开销大每个分片都有额外的头部信息而且不支持一些现代编码特性。fMP4 格式的优点是封装效率高支持更灵活的编码配置而且和 DASH 协议可以共用分片。缺点是部分老设备不支持。我的建议是如果目标设备包含智能电视、机顶盒等传统终端优先用 TS如果主要是现代浏览器和移动端可以用 fMP4。如果拿不准就用 TS它的兼容性下限更高。用 FFmpeg 输出 fMP4 分片的配置如下ffmpeg -i input.mp4 \ -c:v libx264 \ -profile:v high \ -pix_fmt yuv420p \ -c:a aac \ -f hls \ -hls_time 6 \ -hls_playlist_type vod \ -hls_segment_type fmp4 \ -hls_fmp4_init_filename init.mp4 \ -hls_segment_filename segment_%03d.m4s \ output.m3u8注意-hls_segment_type fmp4和-hls_fmp4_init_filename这两个参数fMP4 格式需要一个初始化分片init segment里面包含了解码器配置信息播放器必须先加载它才能解码后续分片。2.4 m3u8 索引文件里的编码声明m3u8 索引文件里的CODECS属性非常重要它告诉播放器这个流用的是什么编码播放器据此判断自己能不能播。如果这个属性写错了或者没写播放器可能会误判导致本来能播的流播不了。一个标准的 H.264 m3u8 索引长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MAP:URIinit.mp4 #EXTINF:6.000, #EXT-X-BITRATE:1200 segment_000.m4s #EXTINF:6.000, #EXT-X-BITRATE:1200 segment_001.m4s #EXT-X-ENDLIST如果是多码率自适应流主索引里会这样标注#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH1500000,CODECSavc1.64001f,mp4a.40.2,RESOLUTION1280x720 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH3000000,CODECShvc1.1.6.L93.B0,mp4a.40.2,RESOLUTION1920x1080 1080p_hevc.m3u8avc1.64001f是 H.264 High Profile Level 3.1 的编码字符串hvc1.1.6.L93.B0是 H.265 Main Profile 的编码字符串。播放器读到CODECS后会调用MediaSource.isTypeSupported()或类似的能力检测接口判断自己能不能解码。如果不支持就会跳过这个流选择其他可用的流。注意CODECS字符串必须和实际编码参数完全一致。如果实际编码是 Main Profile但CODECS写成了 High Profile播放器可能会误判导致播放失败。这个细节很容易被忽略但排查起来非常费劲。3. 浏览器端播放适配的完整实现路径服务端的流准备好了接下来就是浏览器端怎么播。这一步是整个链路里最容易出问题的环节因为浏览器的解码能力差异太大而且错误信息往往非常模糊。我下面按“能力检测 → 播放器选择 → 错误处理 → 降级策略”的顺序把完整的实现路径讲清楚。3.1 播放前的编解码能力检测方法在决定用哪个流之前必须先检测当前浏览器能不能解码。最可靠的方法是使用MediaSource.isTypeSupported()接口传入完整的 MIME 类型字符串返回布尔值。function checkCodecSupport(codecString) { const mimeType video/mp4; codecs${codecString}; if (typeof MediaSource ! undefined MediaSource.isTypeSupported) { return MediaSource.isTypeSupported(mimeType); } return false; } // 检测 H.264 High Profile Level 4.1 const h264Supported checkCodecSupport(avc1.640029); // 检测 H.265 Main Profile const h265Supported checkCodecSupport(hvc1.1.6.L93.B0); console.log(H.264 支持:, h264Supported); console.log(H.265 支持:, h265Supported);这里有个细节要注意isTypeSupported的检测结果只代表浏览器声称支持不代表实际一定能播。有些情况下浏览器返回 true但实际解码时因为硬件或驱动问题失败。所以检测之后还要有运行时错误处理兜底。另外对于 H.265 的检测不同浏览器对 codec 字符串的格式要求可能不同。有的认hvc1有的认hev1有的两个都认。稳妥的做法是两个都测一遍只要有一个返回 true 就认为支持。function checkH265Support() { const variants [ video/mp4; codecshvc1.1.6.L93.B0, video/mp4; codecshev1.1.6.L93.B0, video/mp4; codecshvc1, video/mp4; codecshev1 ]; return variants.some(type { try { return MediaSource.isTypeSupported(type); } catch (e) { return false; } }); }3.2 hls.js 与原生 video 标签的选型对比浏览器端播放 HLS有两条路用原生 video 标签直接播或者用 hls.js 这类 JavaScript 库来播。两者的适用场景完全不同。Safari 和 iOS 上的原生 video 标签原生支持 HLS直接设置src为 m3u8 地址就能播不需要任何库。而且它能利用系统级的硬件解码性能和功耗都最优。所以在苹果生态里优先用原生播放。Chrome、Firefox、Edge 这些浏览器不支持原生 HLS必须借助 hls.js 或类似库。hls.js 的原理是用 JavaScript 把 TS 分片解析出来通过 MSEMedia Source Extensions接口喂给 video 标签。它支持自适应码率切换、错误恢复、字幕等功能是目前最成熟的方案。我的选型策略是这样的const video document.getElementById(video); const videoSrc https://example.com/stream.m3u8; if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 或 iOS原生支持 HLS video.src videoSrc; } else if (Hls.isSupported()) { // 其他浏览器用 hls.js const hls new Hls({ enableWorker: true, lowLatencyMode: false, maxBufferLength: 30, maxMaxBufferLength: 60 }); hls.loadSource(videoSrc); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () { video.play().catch(e console.log(自动播放被拦截:, e)); }); } else { console.error(当前浏览器不支持 HLS 播放); }video.canPlayType(application/vnd.apple.mpegurl)是判断原生 HLS 支持的标准方法。返回maybe或probably都表示支持。这个方法在 Safari 上返回maybe在 Chrome 上返回空字符串。3.3 hls.js 的关键配置项与调优hls.js 的默认配置能应付大部分场景但在一些特殊情况下需要调优。我挑几个最常调整的参数说说。maxBufferLength控制缓冲区最大长度默认 30 秒。如果你的流码率很高或者用户网络不稳定可以适当调大减少卡顿。但调太大也会增加内存占用和起播延迟。maxMaxBufferLength是缓冲区的硬上限默认 600 秒。一般不用动除非你有特殊需求。enableWorker开启后hls.js 会把解析工作放到 Web Worker 里避免阻塞主线程。对于高码率流建议开启。lowLatencyMode是低延迟模式适合直播场景。点播场景不要开会增加不必要的开销。fragLoadingMaxRetry和manifestLoadingMaxRetry控制加载失败的重试次数。默认值偏保守网络环境差的话可以调大。const hlsConfig { enableWorker: true, lowLatencyMode: false, maxBufferLength: 30, maxMaxBufferLength: 60, fragLoadingMaxRetry: 6, manifestLoadingMaxRetry: 4, levelLoadingMaxRetry: 4, fragLoadingRetryDelay: 1000, manifestLoadingRetryDelay: 1000, startLevel: -1, // 自动选择起始码率 abrEwmaDefaultEstimate: 500000 // 初始带宽估计 500kbps }; const hls new Hls(hlsConfig);startLevel: -1表示自动选择起始码率hls.js 会根据网络状况选一个合适的。如果你希望固定从某个码率开始可以设成对应的 level 索引。abrEwmaDefaultEstimate是初始带宽估计值影响起播时的码率选择。设得太高会导致起播卡顿设得太低会导致画质差。500kbps 是个比较稳妥的默认值。3.4 播放错误的捕获与降级处理浏览器播放视频出错时video 元素会触发error事件但error对象的信息非常有限只有一个code和message。常见的错误码有这几个错误码含义常见原因1MEDIA_ERR_ABORTED用户主动中止播放2MEDIA_ERR_NETWORK网络错误导致下载中断3MEDIA_ERR_DECODE解码错误编码格式不支持4MEDIA_ERR_SRC_NOT_SUPPORTED源格式不支持MEDIA_ERR_DECODE和MEDIA_ERR_SRC_NOT_SUPPORTED是兼容性问题最常触发的两个错误。遇到这两个错误基本可以确定是编码格式的问题。hls.js 有自己的错误处理机制会触发Hls.Events.ERROR事件错误对象里包含type、details、fatal等信息。fatal: true表示致命错误播放无法继续需要降级处理。hls.on(Hls.Events.ERROR, (event, data) { console.error(HLS 错误:, data.type, data.details, data.fatal); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: // 网络错误尝试重新加载 hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: // 媒体错误尝试恢复 hls.recoverMediaError(); break; default: // 无法恢复销毁并降级 hls.destroy(); fallbackToH264(); break; } } }); function fallbackToH264() { // 切换到 H.264 流 const h264Src https://example.com/stream_h264.m3u8; if (Hls.isSupported()) { const newHls new Hls(); newHls.loadSource(h264Src); newHls.attachMedia(video); } else { video.src h264Src; } }降级策略的核心思路是先尝试恢复恢复不了就换流。recoverMediaError()会尝试重新初始化解码器对于偶发的解码错误有效。如果是编码格式根本不支持那就只能换 H.264 流。实操心得降级逻辑一定要做而且要在用户无感知的情况下完成。我见过一些项目H.265 流播不了就直接报错用户看到黑屏就流失了。正确的做法是静默切换到 H.264 流用户最多感觉到画质略有变化但不会中断观看。4. 移动端与特殊平台的适配要点浏览器端的适配讲完了但流媒体的战场远不止浏览器。移动端的 ExoPlayer/Media3、智能电视、机顶盒每个平台都有自己的脾气。这一章我重点讲 Android 端的适配因为这是除浏览器外最常见的场景。4.1 Android Media3 ExoPlayer 播放 HLS 的配置Android 上播放 HLS目前官方推荐的是 Media3 ExoPlayer。它对 HLS 的支持比较完善但 H.265 的支持仍然依赖设备硬解。配置上需要注意几个点。首先是依赖引入Media3 的 HLS 模块是独立的需要单独引入implementation androidx.media3:media3-exoplayer:1.2.0 implementation androidx.media3:media3-exoplayer-hls:1.2.0 implementation androidx.media3:media3-ui:1.2.0然后是播放器的创建和配置val trackSelector DefaultTrackSelector(context).apply { setParameters( buildUponParameters() .setMaxVideoSizeSd() .setAllowVideoMixedMimeTypeAdaptiveness(true) .setAllowVideoNonSeamlessAdaptiveness(true) ) } val player ExoPlayer.Builder(context) .setTrackSelector(trackSelector) .setMediaSourceFactory( DefaultMediaSourceFactory(context).setLiveTargetOffsetMs(5000) ) .build() val mediaItem MediaItem.fromUri(https://example.com/stream.m3u8) player.setMediaItem(mediaItem) player.prepare() player.play()setAllowVideoMixedMimeTypeAdaptiveness(true)这个配置很关键。它允许播放器在自适应码率切换时在不同编码格式的流之间切换。比如从 H.265 流切到 H.264 流如果这个选项没开切换会失败。4.2 设备解码能力查询与流选择Android 设备的解码能力差异极大同一个应用在不同手机上表现可能完全不同。所以在选择流之前必须先查询设备的解码能力。Media3 提供了MediaCodecInfo接口来查询fun isCodecSupported(mimeType: String): Boolean { val codecList MediaCodecList(MediaCodecList.REGULAR_CODECS) return codecList.codecInfos.any { codecInfo - !codecInfo.isEncoder codecInfo.supportedTypes.any { it.equals(mimeType, ignoreCase true) } } } val h264Supported isCodecSupported(video/avc) val h265Supported isCodecSupported(video/hevc) Log.d(CodecCheck, H.264: $h264Supported, H.265: $h265Supported)video/avc是 H.264 的 MIME 类型video/hevc是 H.265 的。查询到支持情况后就可以决定加载哪个 m3u8 索引。但要注意MediaCodecList返回的是设备声称支持的解码器实际能不能用还要看具体参数。比如设备支持 H.265但只支持 Main Profile不支持 Main 10那 10bit 的 H.265 流还是播不了。所以查询之后最好再用MediaCodecInfo.CodecCapabilities做更细粒度的判断。4.3 移动端常见的兼容性坑与规避移动端的坑比浏览器端更多我挑几个最典型的说说。第一个坑是 H.265 的 profile 不匹配。很多设备支持 H.265 Main Profile但不支持 Main 10。如果你的流是 10bit 编码的设备会直接报解码错误。规避方法是服务端转码时统一用 8bit或者准备两套流。第二个坑是音频编码。HLS 的音频一般是 AAC但有些设备对 AAC 的 profile 也有要求。LC-AAC 兼容性最好HE-AAC 虽然省带宽但部分老设备不支持。如果遇到有画面没声音的情况先检查音频编码。第三个坑是分片时长。移动网络不稳定分片太长容易卡顿太短又增加请求开销。一般建议 4 到 6 秒直播场景可以短到 2 秒。但有些设备对分片时长有硬性要求比如必须能被某个数整除这个要在实际测试中验证。第四个坑是 HTTPS 混合内容。如果你的页面是 HTTPS 的m3u8 和分片也必须是 HTTPS否则会被浏览器拦截。这个在浏览器端很常见移动端 WebView 里也一样。提示移动端适配一定要在真机上测模拟器的解码能力和真机差异很大。尤其是 H.265很多模拟器根本不支持测了也白测。5. 典型故障排查实录与速查手册前面讲了原理和配置这一章我整理一些实际排查过的案例把问题现象、排查思路和解决方法列出来方便你遇到类似问题时快速定位。5.1 黑屏无报错类问题的排查路径黑屏是最让人头疼的问题因为浏览器往往不给任何错误信息。遇到黑屏我一般按这个顺序排查。第一步看控制台有没有报错。如果连MEDIA_ERR_SRC_NOT_SUPPORTED都没有说明请求可能根本没发出去或者被拦截了。检查 m3u8 的 URL 是否正确跨域配置是否允许。第二步看网络面板。m3u8 请求返回了吗分片请求返回了吗返回的状态码是什么如果 m3u8 返回 200 但分片返回 404说明切片路径配置有问题。第三步看 m3u8 内容。把 m3u8 下载下来检查CODECS属性是否正确分片路径是否可访问。有时候 m3u8 里的路径是相对路径但实际部署时路径层级变了导致分片加载失败。第四步看编码参数。用ffprobe检查实际编码格式确认和 m3u8 里声明的一致。重点看 profile、level、pix_fmt 这几个参数。ffprobe -v error -select_streams v:0 \ -show_entries streamcodec_name,profile,level,pix_fmt,width,height \ -of defaultnoprint_wrappers1 input.ts第五步用最小化测试。写一个最简单的 HTML 页面只放一个 video 标签直接播 m3u8。排除掉业务代码的干扰看能不能播。如果最小化测试能播说明问题在业务代码里如果也不能播说明是流本身的问题。5.2 H.265 在 Chrome 上无法播放的定位方法H.265 在 Chrome 上播不了是最常见的兼容性问题。定位方法如下。首先确认 Chrome 版本和操作系统。Chrome 从 107 版本开始支持 H.265 硬解但需要 Windows 10 以上、显卡支持 HEVC 硬解、并且安装了 HEVC 视频扩展。这三个条件缺一不可。然后检查MediaSource.isTypeSupported的返回值。如果返回 false说明 Chrome 认为自己不支持那就别折腾了直接降级到 H.264。如果返回 true 但实际播不了说明是运行时解码失败需要看具体的错误信息。在 Chrome 地址栏输入chrome://media-internals可以看到详细的媒体播放日志。里面会记录解码器的初始化过程、失败原因等信息。这个工具非常有用但知道的人不多。还有一个方法是看chrome://gpu里面会列出当前系统支持的硬件解码格式。如果 HEVC 那一项显示Hardware accelerated说明支持硬解如果显示Software only或Disabled说明不支持。5.3 常见问题速查表我把常见的兼容性问题整理成了一张速查表遇到问题可以先在这里对号入座。问题现象可能原因排查方法解决方案黑屏无报错m3u8 路径错误或跨域看网络面板请求状态修正路径配置 CORSMEDIA_ERR_DECODE编码格式不支持ffprobe 检查编码参数降级到 H.264有画面无声音音频编码不兼容检查音频 codec改用 LC-AACSafari 播不了 H.265tag 是 hev1 不是 hvc1检查 MP4 tag转码时加 -tag:v hvc1起播卡顿起始码率过高看 abrEwmaDefaultEstimate调低初始带宽估计播放中途卡顿缓冲区太小看 maxBufferLength适当调大缓冲区画面花屏关键帧间隔不对检查 -g 参数设为帧率的整数倍音画不同步时间戳问题检查 PTS/DTS重新转码确保时间戳连续5.4 独家避坑经验分享最后分享几个我在实际项目中踩过的坑都是文档里不会写的。第一个坑不要迷信isTypeSupported的返回值。有些浏览器返回 true但实际解码时因为驱动问题失败。所以检测之后一定要有运行时错误处理不能只靠检测结果。第二个坑H.265 的 level 要留余量。比如你的视频是 1080p 30fps理论上 Level 4.0 就够了但有些设备对 level 的解析比较严格建议用 Level 4.1 或更高留一点余量。第三个坑m3u8 的EXT-X-VERSION要匹配。如果用了 fMP4 分片版本号至少要 7如果用了EXT-X-MAP版本号至少要 6。版本号写低了播放器可能不认某些标签。第四个坑CDN 缓存要配置正确。m3u8 索引文件不能缓存太久否则更新不及时分片文件可以缓存很久因为它们是不可变的。如果 CDN 把 m3u8 缓存了直播场景会出现拉不到最新分片的问题。第五个坑测试要覆盖真实设备。我在模拟器上测 H.265 好好的一到真机就播不了因为模拟器用的是软件解码真机用的是硬件解码两者行为不一致。所以移动端适配一定要在真机上测而且要覆盖不同品牌、不同价位的设备。第六个坑降级逻辑要幂等。如果降级到 H.264 之后还是播不了不要再无限降级要有终止条件。我见过一个项目降级逻辑写成了死循环用户设备不支持任何格式时页面直接卡死。第七个坑日志要打全。兼容性问题排查最怕信息不足。建议在播放器的关键节点都打上日志包括能力检测结果、流选择结果、错误事件、降级触发等。这些日志在排查线上问题时非常有用。我在实际使用中发现流媒体适配这件事80% 的问题都出在编码格式和封装格式的细节上剩下 20% 是网络和配置问题。把编码参数配对把 m3u8 写对把降级逻辑做扎实大部分兼容性问题都能解决。真正难的是那些偶发的、和具体设备相关的边缘情况这些只能靠充分的测试和完整的日志来覆盖。