以ExoPlayer为例,是如何解析url然后将视频帧画面渲染到TexureView上

249 阅读4分钟

一、从 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选择(简要细节)

  1. 你调用 player.setMediaItem(mediaItem)。

  2. MediaSourceFactory.createMediaSource(mediaItem):

    • 若 mediaItem.localConfiguration.mimeType 存在,用它;

    • 否则 Util.inferContentType(uri/extension):

      • .m3u8 → HlsMediaSource
      • .mpd  → DashMediaSource
      • 其它 → ProgressiveMediaSource(配合 DefaultExtractorsFactory 选择 mp4/mkv/ts 等解析器)
  3. DataSource(DefaultHttpDataSource / OkHttpDataSource / CronetDataSource / 文件)去拉数据。

  4. 清单类(HLS/DASH)再创建 ChunkSource,按自适应码率拉分片;

    Progressive 则直接流读 + Extractor 分离音视频轨。

  5. TrackSelector 根据能力/带宽选具体视频轨。

  6. 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 生命周期布局动画裁剪,就是稳定不黑屏的关键。