一、从 URL 到 TextureView 的完整链路
MediaItem(URL/MIME) ← 你设置的播放地址
│
ExoPlayer.setMediaItem()
│
MediaSourceFactory (根据 URL/MIME 推断类型)
├─ ProgressiveMediaSource (mp4/mkv/ts 等文件流 → Extractors)
├─ HlsMediaSource (.m3u8 → HLS 清单/分片)
├─ DashMediaSource (.mpd → DASH 清单/分片)
└─ RtspMediaSource / Rtmp... (相应模块)
│
DataSource(HTTP/文件/缓存) + Extractor/ChunkSource
│
TrackSelector 选轨(视频/音频/字幕)
│
Renderers
├─ MediaCodecVideoRenderer → 解码输出到 Surface
└─ MediaCodecAudioRenderer → 音频轨播放
│
Surface(由 TextureView 的 SurfaceTexture 包装而来)
│
SurfaceTexture(消费者,把帧变为 OES 外部纹理)
│
TextureView(在 View 树中用 GPU 采样这张外部纹理进行绘制)
关键点:
- URL 解析:ExoPlayer 会优先用 MediaItem.mimeType,没有的话用后缀/响应头 推断内容类型,从而选用对应的 MediaSource。
- 视频输出:MediaCodecVideoRenderer 在 configure()/setOutputSurface() 时把 解码输出 Surface 指向你提供的 Surface(这个 Surface 背后连着 SurfaceTexture)。
- 呈现时序:ExoPlayer 用 VideoFrameReleaseHelper 协同 MediaCodec.releaseOutputBuffer(..., presentationTimeUs),按显示刷新率/时序把帧送到 Surface,减少抖动与卡顿。
- 为什么一定要 Surface:绝大多数“产帧方”(硬解码/播放器/相机/GL)只认识 Surface;TextureView 内部持有的是 SurfaceTexture(消费者) ,所以需要 Surface(surfaceTexture) 这一步来“接通管道”。
二、最小可用代码
方案 A:直接使用TextureView(不依赖 PlayerView)
适合你已有自定义 UI,只要把视频画面输出到 TextureView。
class VideoActivity : AppCompatActivity() {
private lateinit var player: ExoPlayer
private lateinit var textureView: TextureView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video)
textureView = findViewById(R.id.textureView)
player = ExoPlayer.Builder(this)
// 可选:自定义 DataSourceFactory(OkHttp/Cronet/缓存)
//.setMediaSourceFactory(DefaultMediaSourceFactory(myDataSourceFactory))
.build()
val item = MediaItem.Builder()
.setUri("https://example.com/stream.m3u8") // 或 mp4 等
//.setMimeType(MimeTypes.APPLICATION_M3U8) // 有需要可显式声明
.build()
player.setMediaItem(item)
player.prepare()
// 把 TextureView 交给播放器(内部会用其 SurfaceTexture 创建 Surface)
player.setVideoTextureView(textureView)
player.playWhenReady = true
// TextureView 可能还没 ready;可选地监听回调确保时序
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
// 一般对“解码器→TextureView”的路径 **不需要** setDefaultBufferSize
// player.setVideoTextureView(textureView) 已经足够
}
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {}
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
player.setVideoTextureView(null) // 断开,避免 bad surface
return true // 让系统回收底层资源
}
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
}
}
override fun onStop() {
super.onStop()
player.pause()
}
override fun onDestroy() {
super.onDestroy()
player.release()
}
}
也可以手动构造:
val surface = Surface(textureView.surfaceTexture) → player.setVideoSurface(surface);
但推荐用 setVideoTextureView(textureView),Exo 会替你处理重建、旋转、清空等时序。
方案 B:PlayerView Compose 一把梭XMLPlayerView指定纹理输出)
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:surface_type="texture_view" <!-- 关键:让内部用 TextureView 而非 SurfaceView -->
app:use_controller="true"/>
playerView.player = player
Compose(Media3PlayerSurface)
@Composable
fun VideoSurface(player: Player) {
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_TEXTURE_VIEW, // 或 SURFACE_TYPE_SURFACE_VIEW
modifier = Modifier.fillMaxWidth().aspectRatio(16/9f)
)
}
三、URL 解析与MediaSource选择(简要细节)
-
你调用 player.setMediaItem(mediaItem)。
-
MediaSourceFactory.createMediaSource(mediaItem):
-
若 mediaItem.localConfiguration.mimeType 存在,用它;
-
否则 Util.inferContentType(uri/extension):
- .m3u8 → HlsMediaSource
- .mpd → DashMediaSource
- 其它 → ProgressiveMediaSource(配合 DefaultExtractorsFactory 选择 mp4/mkv/ts 等解析器)
-
-
DataSource(DefaultHttpDataSource / OkHttpDataSource / CronetDataSource / 文件)去拉数据。
-
清单类(HLS/DASH)再创建 ChunkSource,按自适应码率拉分片;
Progressive 则直接流读 + Extractor 分离音视频轨。
-
TrackSelector 根据能力/带宽选具体视频轨。
-
MediaCodecVideoRenderer/AudioRenderer 消耗对应轨的样本,视频轨解码输出到 Surface。
四、TextureView 路径下的常见坑
-
只留 Surface,不留 TextureView/SurfaceTexture 强引用
Surface 对其消费者是“弱关联”,消费者销毁后继续写会 Surface … is abandoned/bad surface。用 setVideoTextureView()/clearVideoTextureView() 让 Exo 管理好引用与解绑时序。
-
父容器做高度动画把视频裁掉
使用 animateContentSize() 或 AnimatedVisibility 控制布局过渡,避免在父容器硬裁剪 TextureView;视频区建议加 aspectRatio() 保持比例。
-
首帧“只显示一部分/跳一下”
通常是布局测量/裁剪或过渡动画造成,非解码问题。确保视频视图有稳定尺寸,必要时在首帧前盖占位层,player.videoSize 到齐后再去掉。
-
低端机性能抖动
TextureView 需要 GPU 再采样一遍,比 SurfaceView 稍重。对延迟/稳定极敏感的播放建议 SurfaceView;但需要圆角/缩放/变换时选 TextureView。
五、释放与重建建议
- 切后台/销毁:player.clearVideoTextureView()(或 setVideoTextureView(null))→ player.release();
- 切换全屏/旋转:PlayerView 会替你处理 Surface 重建。自己托管 TextureView 时,注意在 onSurfaceTextureDestroyed 解绑,在 Available 后再绑定。
- 变更输出目标:Exo 内部支持 setVideoSurface(null) → setVideoSurface(new) 的 无缝切换(API 23+ 用 MediaCodec#setOutputSurface),尽量走播放器 API,不要直接对 MediaCodec 动手。
总结
- Exo 根据 URL/MIME 选 MediaSource(HLS/DASH/Progressive…)→ 读取与解复用 → 交给 MediaCodecVideoRenderer。
- 你把 TextureView 交给播放器(setVideoTextureView() 或 PlayerView 的 app:surface_type="texture_view"),Exo 就会把 解码输出 Surface 指向由 SurfaceTexture 包装的 Surface,最后由 TextureView 在 View 树中绘制出来。
- 管好 Surface/TextureView 生命周期 与 布局动画裁剪,就是稳定不黑屏的关键。