一、为什么会有"有声音没画面 / 黑屏"
本质:视频渲染链 的某个环节没把帧按时送到屏幕,但音频链仍在正常工作。
A. 视图/Surface 管线问题
-
Surface 丢失或没绑定
- SurfaceView/TextureView 生命周期没对齐:surfaceDestroyed/onSurfaceTextureDestroyed 之后仍在解码;或未及时 setVideoSurface(...)。
- 解决:销毁前 先 player.clearVideoSurface() / setVideoSurface(null),创建后再绑定。
-
TextureView 黑屏/机型兼容
- 需要硬件加速;部分机型(尤其老设备/某些厂商)TextureView + 某些硬件解码器组合会黑屏。
- 解决:优先 SurfaceView 做大面积视频;TextureView 必要时 isOpaque = true 并确保启用硬件加速。
-
层级/透明度/裁剪出错
- alpha=0、透明蒙层盖住、错误的 setTransform(Matrix) 把画面移出可视区。
- 解决:临时打印/强制 view.alpha=1f、去掉复杂变换验证。
-
过多 Surface / 复用不当(RecyclerView 多视频)
-
同时活跃的 Surface 太多导致 buffer 资源紧张,或视图回收时没解绑。
-
解决:单播放器多 View 复用、可见即绑定、不可见立即 clearVideoSurface();限制并发可见视频数。
-
B. 媒体与解码相关
-
码流不兼容 / 解码器异常
- HEVC 10-bit、Profile/Level 过高、奇怪色彩格式(yuv444)等设备不支持;或解码器崩溃/复位。
- 解决:短视频分发统一编码档(下文提供),或捕获 onDecoderInitializationError 回退。
-
容器/清单问题
- HLS/DASH 选到了 audio-only 变体(就会"有声无画");MP4 moov 在尾导致首帧迟迟不出。
- 解决:检查清单/封装;点播 MP4 开启 faststart(moov 前置)。
-
DRM/安全路径要求
- 内容要求 secure decoder + secure surface,但用了 TextureView 或窗口不安全;结果视频被抑制、只出音频或直接错误。
- 解决:DRM 内容用 SurfaceView + FLAG_SECURE(或 SurfaceView#setSecure(true)),保证 Widevine L1 链路。
-
分辨率/旋转切换瞬间的重配置
-
切流/旋转元数据切换,解码器重配置期间短暂黑屏。
-
解决:允许平滑切换、避免频繁切码率/旋转;或在 UI 层做过渡。
-
C. 时序与性能
-
渲染线程被阻塞 / VSync 对不齐
- 主线程卡顿、GPU 饱和、VSync 错位导致帧释放赶不上,表现为长时间卡黑。
- 解决:避免主线程重负载;开启 frame-rate matching(API 30+);降低分辨率/码率。
-
音频欠喂/重缓冲
- 不是黑屏根因,但会触发频繁重缓冲,卡在黑场。
- 解决:合理缓冲阈值与 ABR,见下文调参。
二、如何避免 & 优化(Checklist)
1) 视图与生命周期(必做)
SurfaceView 版本:
kotlin
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(h: SurfaceHolder) { player.setVideoSurface(h.surface) }
override fun surfaceDestroyed(h: SurfaceHolder) { player.clearVideoSurface() }
})
TextureView 版本(仅在需要动画/圆角时用):
kotlin
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
player.setVideoSurface(Surface(st))
}
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
player.clearVideoSurface(); return true
}
}
-
Activity/Fragment 销毁或 View 被回收时,一定记得 解绑 Surface。
-
RecyclerView 场景:可见才绑定,不可见立即解绑,限制并发可见视频数(例如 ≤2)。
2) ExoPlayer 配置(稳)
scss
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(1500, 5000, 500, 1000) // 起播快、稳态不容易欠喂
.build()
val player = ExoPlayer.Builder(context)
.setLoadControl(loadControl)
.setVideoChangeFrameRateStrategy(
VIDEO_CHANGE_FRAME_RATE_STRATEGY_ONLY_IF_SEAMLESS // API 30+ 请求匹配刷新率
)
.build()
3) 编码与封装规范(强烈建议给短视频统一档)
-
视频 :H.264/AVC High@L4.1 、yuv420p、分辨率 ≤ 1080p(短视频常 540p/720p);关键帧间隔 ≤ 2s。
-
音频:AAC LC,48kHz,立体声。
-
容器 :MP4 moov 前置(faststart) 。
-
HLS/DASH:优先 CMAF fMP4 ;分片 2--6s;确保 每个分片起始是 IDR;避免出现音频专用变体被误选。
4) DRM 内容(如有)
-
使用 SurfaceView 并启用安全输出(Window FLAG_SECURE 或 SurfaceView#setSecure(true))。
-
设备仅 Widevine L3 时,限制清晰度或走不加密备选。
5) 监控与快速定位(很关键)
kotlin
player.addAnalyticsListener(object : AnalyticsListener {
override fun onRenderedFirstFrame(e: EventTime, output: Any, renderTimeMs: Long) { /* 首帧时延 */ }
override fun onDroppedVideoFrames(e: EventTime, count: Int, elapsedMs: Long) { /* 丢帧尖峰 */ }
override fun onAudioUnderrun(e: EventTime, buf: Int, bufMs: Long, elapsedMs: Long) { /* 欠喂 */ }
override fun onDecoderInitializationError(e: EventTime, error: DecoderInitializationException) { /* 解码器问题 */ }
override fun onPlaybackStateChanged(e: EventTime, state: Int) { /* state=READY但无首帧? 看surface绑定 */ }
})
- 重点打点:首帧时间 、rebuffer 次数/时长 、dropped frames 、decoder init error 、是否收到 onRenderedFirstFrame。
6) 其它细节
- TextureView 必须开启 硬件加速,并尽量 isOpaque = true。
- 尽量避免在解码/渲染线程做重负载(大图滤镜、主线程阻塞)。
- 对 24fps 内容在 60Hz 屏上:尽量让系统切到 48/120Hz 或允许 Exo 请求帧率匹配,减少结构性抖动。
- 直播:用 MediaItem.LiveConfiguration 允许轻微超速(≤1.02x)追边缘,避免越播越远导致频繁重缓冲黑场。
三、快速排查路径(出现"有声无画/黑屏"时)
- 看事件 :是否触发 onRenderedFirstFrame?没有 → 多半是 Surface/解码器/封装问题。
- 看错误:onDecoderInitializationError / onVideoCodecError 是否出现?
- 看清单/轨道 :当前是否选到了 audio-only 轨(HLS/DASH)?
- 切视图:把 TextureView 换成 SurfaceView 验证。
- 降档 :换 H.264 Baseline/Main、720p 流验证;问题消失说明是 码流/解码器兼容。
- 打点:dropped frames 是否持续上升且 AudioUnderrun 频繁?→ 性能/缓冲不足。
- DRM:确认是否 L1 要求、是否走了 secure surface;若 L3 设备,降清晰度或走非 DRM 流。
一句话总结
- 有声无画/黑屏 通常来自:Surface 生命周期/绑定问题 、TextureView 兼容/硬件加速 、码流/解码器不兼容 、清单挑到 audio-only 、DRM 安全路径不满足 、或渲染时序/性能问题。
- 预防靠 规范编码 + 正确的 Surface 管理 + 合理的 Player 配置 + 指标监控 ;一旦出问题,按上面 7 步排查,通常几分钟内能定位到具体环节。