VideoPlayer 释放机制详解

1 阅读11分钟

VideoPlayer 释放机制详解

目录


概述

本文档详细说明了基于 GSYVideoPlayer 的视频播放器在切换视频时的资源释放机制,重点解决硬解码场景下 reconfigure_codec_l:configure_surface: failed 的问题。

问题现象

  • ✅ 第一次播放视频:硬解码成功
  • ❌ 播放中切换到下一个视频:reconfigure_codec_l:configure_surface: failed
  • ✅ 完全关闭播放器窗口再打开:硬解码成功

根本原因

旧 MediaCodec 未正确解除与 Surface 的绑定,新 MediaCodec 尝试使用同一个 Surface 时配置失败。


核心组件与职责

1. GSYVideoPlayer 架构层次

┌─────────────────────────────────────────────┐
│         VideoPlayerActivity                 │  应用层
│  - 管理播放器生命周期                        │
│  - 处理视频切换逻辑                          │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│         HHTMediaPlayer                      │  自定义层
│  (extends StandardGSYVideoPlayer)           │
│  - 重写 startPrepare() 优化 Surface 设置    │
│  - 提供 releaseForVideoSwitch() 方法        │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│      GSYTextureRenderView                   │  渲染层
│  - 持有 mSurface 成员变量                   │
│  - 实现 IGSYSurfaceListener 接口            │
│  - 管理 TextureView/SurfaceView 创建        │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│         GSYVideoManager                     │  管理层
│  - 单例管理所有播放器实例                    │
│  - 协调 Surface 与 Player 的绑定            │
│  - 通过 Handler 异步处理 prepare/release    │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│       IjkPlayerManager                      │  播放器层
│  - 封装 IjkMediaPlayer                      │
│  - 管理 Surface 绑定                         │
│  - 控制 MediaCodec 生命周期                  │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│       IjkMediaPlayer (Native)               │  Native 层
│  - IJK FFmpeg 播放器                        │
│  - MediaCodec 硬解码                        │
│  - Surface 渲染输出                          │
└─────────────────────────────────────────────┘

2. 关键成员变量

变量作用
GSYTextureRenderViewprotected Surface mSurface保存当前渲染使用的 Surface 引用
IjkPlayerManagerprivate Surface surfaceIjkPlayer 层面的 Surface 引用
GSYTextureViewprivate Surface mSurfaceTextureView 创建的 Surface 包装
GSYVideoManagerIPlayerManager playerManager当前使用的播放器管理器实例

完整释放流程

场景一:关闭窗口(正常流程)

Activity.finish()
  │
  ├─ onDestroy()
  │    │
  │    ├─ unregisterReceiver()
  │    ├─ HHCastServer.removeDLNAListener()
  │    ├─ HHCastServer.removeFileListener()
  │    │
  │    ├─ videoPlayer.release()  ◄─────┐
  │    │    │                           │
  │    │    ├─ releaseVideos()          │  核心释放流程
  │    │    │    │                      │
  │    │    │    ├─ setStateAndUi(CURRENT_STATE_NORMAL)
  │    │    │    │
  │    │    │    ├─ mTextureViewContainer.removeAllViews()  ◄─── ★ 关键步骤 1
  │    │    │    │    │
  │    │    │    │    └─ TextureView 被移除,触发系统回调
  │    │    │    │           │
  │    │    │    │           └─ TextureView.SurfaceTextureListener
  │    │    │    │               .onSurfaceTextureDestroyed(surface)
  │    │    │    │                      │
  │    │    │    │                      ├─ GSYTextureView.onSurfaceTextureDestroyed()
  │    │    │    │                      │    │
  │    │    │    │                      │    └─ mIGSYSurfaceListener.onSurfaceDestroyed(mSurface)
  │    │    │    │                      │           │
  │    │    │    │                      │           └─ GSYTextureRenderView.onSurfaceDestroyed()
  │    │    │    │                      │                  │
  │    │    │    │                      │                  ├─ setDisplay(null)  ◄─── ★ 关键步骤 2
  │    │    │    │                      │                  │    │
  │    │    │    │                      │                  │    └─ GSYVideoView.setDisplay(null)
  │    │    │    │                      │                  │           │
  │    │    │    │                      │                  │           └─ GSYVideoManager.setDisplay(null)
  │    │    │    │                      │                  │                  │
  │    │    │    │                      │                  │                  └─ 发送 HANDLER_SETDISPLAY 消息
  │    │    │    │                      │                  │                         │
  │    │    │    │                      │                  │                         └─ IjkPlayerManager.showDisplay(null)
  │    │    │    │                      │                  │                                │
  │    │    │    │                      │                  │                                └─ mediaPlayer.setSurface(null)
  │    │    │    │                      │                  │                                       │
  │    │    │    │                      │                  │                                       └─ MediaCodec 解除绑定 ✅
  │    │    │    │                      │                  │
  │    │    │    │                      │                  └─ releaseSurface(surface)
  │    │    │    │                      │                         │
  │    │    │    │                      │                         └─ GSYVideoManager.releaseSurface()
  │    │    │    │                      │
  │    │    │    │                      └─ return true / false (决定是否立即销毁 SurfaceTexture)
  │    │    │    │
  │    │    │    ├─ getGSYVideoManager().setListener(null)  ◄─── ★ 清除监听器
  │    │    │    ├─ getGSYVideoManager().setLastListener(null)
  │    │    │    │
  │    │    │    └─ 清除各种状态标志
  │    │    │
  │    │    └─ 播放器完全释放,状态回到 NORMAL
  │    │
  │    ├─ videoPlayerControllerManager.sendAllVideoStop()
  │    ├─ videoPlayerControllerManager.releaseAllController()
  │    │
  │    └─ super.onDestroy()
  │
  └─ Activity 销毁,所有资源回收

关键时序:

  1. removeAllViews() → Android 系统触发 onSurfaceTextureDestroyed()
  2. setDisplay(null) → MediaCodec 先解除 Surface 绑定
  3. 最后 Activity 销毁时,所有对象被 GC 回收

场景二:切换视频(修复前 - 错误流程)

用户点击"下一个视频"按钮
  │
  ├─ onClick() → updateVideoSource(newUrl, newFileName)
  │    │
  │    └─ detectCodecThenPlay(url, fileName)
  │           │
  │           ├─ releaseCurrentPlayer()  ◄────┐
  │           │    │                           │  错误的释放方式
  │           │    ├─ videoPlayer.setVideoAllCallBack(null)
  │           │    │                          │
  │           │    └─ GSYVideoManager.releaseAllVideos()  ◄─── ❌ 直接释放
  │           │           │                    │
  │           │           └─ GSYVideoBaseManager.releaseMediaPlayer()
  │           │                  │
  │           │                  └─ 发送 HANDLER_RELEASE 消息
  │           │                         │
  │           │                         └─ IjkPlayerManager.release()
  │           │                                │
  │           │                                └─ mediaPlayer.release()
  │           │                                       │
  │           │                                       └─ MediaCodec.release()  ◄─── native 异步释放
  │           │                                              │
  │           │                                              └─ ⚠️ Goke SoC 上可能有延迟
  │           │                                                    Surface 绑定未立即解除!
  │           │
  │           ├─ videoPlayer.showLoadingState()
  │           │
  │           └─ VideoCodecDetector.detectCodecAsync()
  │                  │
  │                  └─ onCodecDetected() → startPlayWithKernel()
  │                         │
  │                         ├─ videoPlayer.hideLoadingState()
  │                         ├─ PlayerFactory.setPlayManager(kernelClass)
  │                         ├─ videoPlayer.setUp(url, ...)
  │                         │
  │                         └─ videoPlayer.startPlayLogic()
  │                                │
  │                                └─ prepareVideo() → startPrepare()
  │                                       │
  │                                       ├─ GSYVideoManager.setListener(this)
  │                                       │
  │                                       ├─ ⚠️ HHTMediaPlayer.startPrepare() 重写逻辑:
  │                                       │    │
  │                                       │    ├─ final Surface surfaceToSet = mSurface;  ◄─── ❌ mSurface 仍是旧 Surface!
  │                                       │    │
  │                                       │    ├─ if (surfaceToSet != null && surfaceToSet.isValid()) {
  │                                       │    │      
  │                                       │    │      ├─ GSYVideoManager.setPlayerInitSuccessListener(...)
  │                                       │    │      │      ↓
  │                                       │    │      └─ onPlayerInitSuccess(player, model) {
  │                                       │    │             mediaPlayer.setSurface(旧 surfaceToSet);  ◄─── ❌ 设置旧 Surface
  │                                       │    │         }
  │                                       │    │   }
  │                                       │    │
  │                                       │    └─ getGSYVideoManager().prepare(...)  ◄─── 开始 prepare
  │                                       │           │
  │                                       │           └─ 发送 HANDLER_PREPARE 消息
  │                                       │                  │
  │                                       │                  └─ initVideo(msg)
  │                                       │                         │
  │                                       │                         ├─ playerManager = getPlayManager()  ◄─── 创建新 IjkPlayerManager
  │                                       │                         │
  │                                       │                         ├─ playerManager.initVideoPlayer(...)
  │                                       │                         │    │
  │                                       │                         │    ├─ mediaPlayer = new IjkMediaPlayer()  ◄─── 新播放器
  │                                       │                         │    │
  │                                       │                         │    ├─ 配置硬解码选项
  │                                       │                         │    │
  │                                       │                         │    ├─ mediaPlayer.setDataSource(url)
  │                                       │                         │    │
  │                                       │                         │    └─ initSuccess(model)  ◄─── 触发 PlayerInitSuccessListener
  │                                       │                         │           │
  │                                       │                         │           └─ mPlayerInitSuccessListener.onPlayerInitSuccess(...)
  │                                       │                         │                  │
  │                                       │                         │                  └─ mediaPlayer.setSurface(旧 Surface)  ◄─── ❌ 问题点
  │                                       │                         │
  │                                       │                         └─ mediaPlayer.prepareAsync()
  │                                       │                                │
  │                                       │                                └─ IJK Native 层
  │                                       │                                       │
  │                                       │                                       ├─ ffmpeg 探测流信息
  │                                       │                                       │
  │                                       │                                       └─ 初始化 MediaCodec
  │                                       │                                              │
  │                                       │                                              └─ MediaCodec.configure(format, 旧Surface, ...)
  │                                       │                                                     │
  │                                       │                                                     └─ ❌ 失败!
  │                                       │                                                           reconfigure_codec_l:configure_surface: failed
  │                                       │                                                           
  │                                       │                                                           原因:旧 MediaCodec 虽然调用了 release()
  │                                       │                                                           但 native 层还未完全释放对 Surface 的占用
  │                                       └─ setStateAndUi(CURRENT_STATE_PREPAREING)
  └─ ❌ 播放失败

问题链条:

  1. releaseCurrentPlayer() 只调用了 GSYVideoManager.releaseAllVideos()
  2. 没有先调用 setDisplay(null) 解除 Surface 绑定
  3. mSurface 成员变量仍然保留着旧 Surface 的引用
  4. startPrepare()PlayerInitSuccessListener 捕获到了旧 mSurface
  5. 新 IjkMediaPlayer 创建后,立即被设置了旧 Surface
  6. 旧 MediaCodec 虽然 release() 了,但 native 释放有延迟
  7. 新 MediaCodec 尝试 configure(format, 旧Surface)同一个 Surface 被两个 MediaCodec 竞争 → 失败

场景三:切换视频(修复后 - 正确流程)

用户点击"下一个视频"按钮
  │
  ├─ onClick() → updateVideoSource(newUrl, newFileName)
  │    │
  │    └─ detectCodecThenPlay(url, fileName)
  │           │
  │           ├─ releaseCurrentPlayer()  ◄────┐
  │           │    │                           │  正确的释放方式
  │           │    └─ videoPlayer.releaseForVideoSwitch()  ◄─── ✅ 专用释放方法
  │           │           │
  │           │           ├─ Step 1: 解除 Surface 绑定  ◄─── ★ 关键步骤 1
  │           │           │    │
  │           │           │    └─ getGSYVideoManager().setDisplay(null)
  │           │           │           │
  │           │           │           └─ 发送 HANDLER_SETDISPLAY(null) 消息
  │           │           │                  │
  │           │           │                  └─ IjkPlayerManager.showDisplay(null)
  │           │           │                         │
  │           │           │                         └─ 旧 mediaPlayer.setSurface(null)
  │           │           │                                │
  │           │           │                                └─ ✅ 旧 MediaCodec 解除 Surface 绑定
  │           │           │                                      Surface 已经"空闲"可复用
  │           │           │
  │           │           ├─ Step 2: 清空 Surface 引用  ◄─── ★ 关键步骤 2
  │           │           │    │
  │           │           │    └─ mSurface = null;  ◄─── ✅ 防止 startPrepare 复用旧 Surface
  │           │           │
  │           │           ├─ Step 3: 清除回调
  │           │           │    │
  │           │           │    └─ setVideoAllCallBack(null);
  │           │           │
  │           │           └─ Step 4: 释放播放器  ◄─── ★ 关键步骤 3
  │           │                  │
  │           │                  └─ GSYVideoManager.releaseAllVideos()
  │           │                         │
  │           │                         └─ IjkPlayerManager.release()
  │           │                                │
  │           │                                └─ 旧 mediaPlayer.release()
  │           │                                       │
  │           │                                       └─ ✅ 旧 MediaCodec 释放(已无 Surface 绑定,安全)
  │           │
  │           ├─ videoPlayer.showLoadingState()
  │           │
  │           └─ VideoCodecDetector.detectCodecAsync()
  │                  │
  │                  └─ onCodecDetected() → startPlayWithKernel()
  │                         │
  │                         ├─ videoPlayer.hideLoadingState()
  │                         ├─ PlayerFactory.setPlayManager(kernelClass)
  │                         ├─ videoPlayer.setUp(url, ...)
  │                         │
  │                         └─ videoPlayer.startPlayLogic()
  │                                │
  │                                └─ prepareVideo() → startPrepare()
  │                                       │
  │                                       ├─ ✅ HHTMediaPlayer.startPrepare() 重写逻辑:
  │                                       │    │
  │                                       │    ├─ final Surface surfaceToSet = mSurface;  ◄─── ✅ mSurface 现在是 null!
  │                                       │    │
  │                                       │    ├─ if (surfaceToSet != null && surfaceToSet.isValid()) {
  │                                       │    │      // ✅ 跳过这个分支
  │                                       │    │  } else {
  │                                       │    │      Log.i(TAG, "★ Surface is null, will use Dummy codec first");
  │                                       │    │  }
  │                                       │    │
  │                                       │    └─ getGSYVideoManager().prepare(...)
  │                                       │           │
  │                                       │           └─ initVideo()
  │                                       │                  │
  │                                       │                  ├─ mediaPlayer = new IjkMediaPlayer()  ◄─── 新播放器
  │                                       │                  │
  │                                       │                  ├─ mediaPlayer.setDataSource(url)
  │                                       │                  │
  │                                       │                  └─ mediaPlayer.prepareAsync()  ◄─── ✅ 无 Surface 的情况下开始
  │                                       │                         │
  │                                       │                         └─ IJK 使用 Dummy Surface 或延迟 configure
  │                                       │
  │                                       └─ setStateAndUi(CURRENT_STATE_PREPAREING)
  │
  ├─ ⏳ 等待 prepare 完成...
  │
  └─ onPrepared()  ◄─── GSYVideoView.onPrepared()
         │
         └─ startAfterPrepared()
                │
                ├─ addTextureView()  ◄─── ★ 关键步骤 4:创建全新 TextureView
                │    │
                │    └─ GSYTextureRenderView.addTextureView()
                │           │
                │           └─ mTextureView = new GSYRenderView()
                │                  │
                │                  └─ GSYRenderView.addView(...)
                │                         │
                │                         └─ GSYTextureView.addTextureView(...)
                │                                │
                │                                ├─ textureView = new GSYTextureView(...)
                │                                │
                │                                ├─ textureView.setSurfaceTextureListener(this)
                │                                │
                │                                └─ mTextureViewContainer.addView(textureView)
                │                                       │
                │                                       └─ ⏳ TextureView 被添加到视图树,等待 Surface 创建...
                │
                └─ ⏰ Android 系统异步创建 SurfaceTexture
                       │
                       └─ TextureView.SurfaceTextureListener
                          .onSurfaceTextureAvailable(surfaceTexture, width, height)  ◄─── ★ 系统回调
                                 │
                                 └─ GSYTextureView.onSurfaceTextureAvailable()
                                        │
                                        ├─ mSurface = new Surface(全新 surfaceTexture)  ◄─── ✅ 创建全新 Surface!
                                        │
                                        └─ mIGSYSurfaceListener.onSurfaceAvailable(mSurface)
                                               │
                                               └─ GSYTextureRenderView.onSurfaceAvailable()
                                                      │
                                                      └─ pauseLogic(surface, true)
                                                             │
                                                             ├─ mSurface = surface;  ◄─── ✅ 更新 mSurface 为全新 Surface
                                                             │
                                                             └─ setDisplay(mSurface)
                                                                    │
                                                                    └─ GSYVideoManager.setDisplay(全新 Surface)
                                                                           │
                                                                           └─ IjkPlayerManager.showDisplay(全新 Surface)
                                                                                  │
                                                                                  └─ 新 mediaPlayer.setSurface(全新 Surface)
                                                                                         │
                                                                                         └─ ✅ 新 MediaCodec.configure(format, 全新 Surface)
                                                                                                │
                                                                                                └─ ✅ 成功!
                                                                                                      硬解码配置完成
                                                                                                      开始渲染输出

成功关键:

  1. Step 1: setDisplay(null) → 旧 MediaCodec 先解除 Surface 绑定
  2. Step 2: mSurface = null → 阻止 startPrepare() 复用旧 Surface
  3. Step 3: releaseAllVideos() → 安全释放旧播放器
  4. Step 4: addTextureView() → 创建全新 TextureView 和 Surface
  5. 系统回调: onSurfaceTextureAvailable() → 全新 Surface 设置到新播放器
  6. 结果: 新 MediaCodec 使用全新 Surface → 无冲突 → 成功

三种场景对比

维度关闭窗口再打开修复前切换视频修复后切换视频
触发方式Activity.finish()onDestroy()onClick()updateVideoSource()onClick()updateVideoSource()
清理入口videoPlayer.release()releaseCurrentPlayer()releaseCurrentPlayer()
是否调用 setDisplay(null)✅ 自动(onSurfaceDestroyed()❌ 没有✅ 手动(releaseForVideoSwitch()
mSurface 状态随 Activity 销毁❌ 仍指向旧 Surface✅ 手动置为 null
旧 MediaCodec Surface 绑定✅ 先解除❌ 未解除就 release✅ 先解除
新 Surface 来源新 Activity → 新 TextureView❌ 复用旧 SurfaceaddTextureView() 创建全新
新 MediaCodec configure✅ 使用全新 Surface❌ 使用旧 Surface 冲突✅ 使用全新 Surface
结果✅ 成功❌ 失败✅ 成功

时序图对比

修复前(错误)
releaseAllVideos()        mSurface仍是旧的          startPrepare()
       ↓                        ↓                         ↓
旧MediaCodec.release()    PlayerInitSuccessListener   新MediaCodec.configure()
       ↓                        ↓                         ↓
  native释放延迟        setSurface(旧Surface)        ❌ 冲突失败
       ↓                        ↓
  Surface仍被占用         新播放器绑定旧Surface
修复后(正确)
setDisplay(null)         mSurface = null        releaseAllVideos()
       ↓                        ↓                      ↓
旧MediaCodec解绑        阻止复用旧Surface       安全释放
       ↓                        ↓                      ↓
  Surface空闲          startPrepare()跳过设置      新播放器
       ↓                        ↓                      ↓
  onPrepared()          addTextureView()        全新Surface
       ↓                        ↓                      ↓
  onSurfaceAvailable()   setDisplay(新Surface)   ✅ 成功

Surface 生命周期

Android TextureView Surface 创建流程

TextureView 被添加到视图树
  ↓
View.onAttachedToWindow()
  ↓
ViewRootImpl.performTraversals()
  ↓
TextureView.onVisibilityChanged(VISIBLE)
  ↓
TextureView 内部创建 SurfaceTexture(异步)
  ↓
SurfaceTextureListener.onSurfaceTextureAvailable(surfaceTexture, w, h)
  ↓
应用层创建 Surface 包装
  mSurface = new Surface(surfaceTexture)
  ↓
传递给 MediaPlayer
  mediaPlayer.setSurface(mSurface)
  ↓
传递给 MediaCodec
  mediaCodec.configure(format, mSurface, ...)
  ↓
GPU 渲染输出到 Surface

Surface 销毁流程

TextureView 被从视图树移除(removeView)
  ↓
TextureView.onDetachedFromWindow()
  ↓
SurfaceTextureListener.onSurfaceTextureDestroyed(surfaceTexture)
  ↓
应用层处理
  ├─ mediaPlayer.setSurface(null)  ← 先解绑
  ├─ surface.release()              ← 释放 Java 引用
  └─ return true/false             ← 是否立即释放 SurfaceTexture
  ↓
SurfaceTexture 释放(如果返回 true

Surface 复用机制(硬解码模式)

GSYTextureView.onSurfaceTextureAvailable() 中:

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    if (GSYVideoType.isMediaCodecTexture()) {
        // 硬解码模式:复用 SurfaceTexture
        if (mSaveTexture == null) {
            mSaveTexture = surface;           // 第一次保存
            mSurface = new Surface(surface);
        } else {
            setSurfaceTexture(mSaveTexture); // 后续复用
        }
        mIGSYSurfaceListener.onSurfaceAvailable(mSurface);
    } else {
        // 软解码模式:每次创建新 Surface
        mSurface = new Surface(surface);
        mIGSYSurfaceListener.onSurfaceAvailable(mSurface);
    }
}

为什么硬解码模式要复用 SurfaceTexture?

  • MediaCodec 硬解码时,SurfaceTexture 重建可能导致花屏或黑屏
  • 复用可以保持 GPU 纹理一致性
  • 但这也是问题根源:切换视频时如果不先解绑,会导致 Surface 冲突

MediaCodec 绑定机制

MediaCodec 生命周期

MediaCodec.createByCodecName("编码器名")
  ↓
MediaCodec.configure(format, surface, crypto, flags)  ← Surface 在这里绑定
  ↓
MediaCodec.start()
  ↓
MediaCodec.dequeueInputBuffer() / queueInputBuffer()
  ↓
MediaCodec.dequeueOutputBuffer() / releaseOutputBuffer(index, true) → 渲染到 Surface
  ↓
MediaCodec.stop()
  ↓
MediaCodec.release()  ← Surface 在这里解绑(但可能有延迟)

Surface 绑定规则

  1. 一个 Surface 同时只能被一个 MediaCodec 占用
  2. configure() 时绑定,release() 时解绑
  3. 重要release() 是异步的,native 层可能延迟释放
  4. 在 Goke SoC 上,release() 后立即复用同一个 Surface 会失败

IJK 中的 MediaCodec 使用

// IJKMediaPlayer native 代码(简化)

// 配置 MediaCodec
AMediaCodec_configure(codec, format, window, NULL, 0);

// window 就是从 Java 传来的 Surface 对应的 ANativeWindow
// 一旦 configure 成功,这个 window 就被 MediaCodec 独占

// 解码输出
AMediaCodec_releaseOutputBuffer(codec, index, true); // 渲染到 Surface

// 释放
AMediaCodec_stop(codec);
AMediaCodec_delete(codec);  // 这里会释放对 window 的占用,但可能不是立即的

问题场景重现

时间线:
T0: 旧 MediaCodec.configure(format, Surface_A)  ← Surface_A 被占用
T1: 播放视频1...
T2: 用户点击"下一个"
T3: 旧 MediaCodec.release()  ← 开始释放,但 native 异步
T4: 新 MediaCodec.configure(format, Surface_A)  ← ❌ Surface_A 仍被占用!
T5: configure 失败:reconfigure_codec_l:configure_surface: failed

正确的释放顺序

时间线:
T0: mediaPlayer.setSurface(null)   先解绑!
T1:  MediaCodec.configure(format, null)   MediaCodec 不再占用 Surface_A
T2:  MediaCodec.release()   安全释放
T3: addTextureView()   创建全新 TextureView
T4: 系统回调 onSurfaceTextureAvailable()   创建 Surface_B(全新)
T5:  mediaPlayer.setSurface(Surface_B)
T6:  MediaCodec.configure(format, Surface_B)    成功!

问题根因分析

根本原因

MediaCodec 对 Surface 的独占性 + native 层释放延迟 + 应用层 Surface 复用逻辑冲突

详细分析

  1. GSYVideoPlayer 的设计假设

    • 假设 Surface 的生命周期由 TextureView 管理
    • 正常退出时,removeAllViews() 会触发 onSurfaceTextureDestroyed() 自动清理
    • 但切换视频时 TextureView 并没有被移除,所以没有触发清理回调
  2. HHTMediaPlayer 的优化适得其反

    // HHTMediaPlayer.startPrepare() 中的优化
    final Surface surfaceToSet = mSurface;
    if (surfaceToSet != null && surfaceToSet.isValid()) {
        // 提前设置 Surface 避免黑屏
        GSYVideoManager.instance().setPlayerInitSuccessListener(...);
    }
    
    • 原意:提前设置 Surface,避免 onPrepared() 后才创建 TextureView 导致的短暂黑屏
    • 副作用:切换视频时,mSurface 仍是旧的,导致新播放器绑定旧 Surface
  3. IjkMediaPlayer native 层的异步性

    • Java 层调用 mediaPlayer.release() 立即返回
    • native 层可能在另一个线程中慢慢释放资源
    • MediaCodec 的 AMediaCodec_delete() 也是异步的
    • Goke SoC 的实现可能比高通、MTK 更慢
  4. 硬解码模式的 SurfaceTexture 复用

    • GSYTextureView 在硬解码模式下会复用 mSaveTexture
    • 即使 addTextureView() 创建新 TextureView,底层 SurfaceTexture 可能还是同一个
    • 导致 Surface 引用的底层资源冲突

为什么关闭窗口就没问题?

关闭窗口的完整清理链路:

Activity.onDestroy()
  ↓
mTextureViewContainer.removeAllViews()  ← 移除 TextureView
  ↓
onSurfaceTextureDestroyed()  ← Android 系统回调
  ↓
setDisplay(null)  ← 自动调用,解除绑定
  ↓
releaseSurface(surface)
  ↓
releaseAllVideos()  ← 最后才释放播放器
  ↓
Activity 对象销毁  ← mSurface 随之销毁

下次打开新 Activity:
  ↓
全新的 VideoPlayerActivity 实例
  ↓
全新的 HHTMediaPlayer 实例(mSurface 初始为 null)
  ↓
全新的 TextureView
  ↓
全新的 SurfaceTexture
  ↓
全新的 Surface
  ↓
✅ 完全隔离,不会冲突

修复方案

修复原则

模拟 Activity 销毁时的完整清理流程

代码实现

HHTMediaPlayer.java
/**
 * 切换视频前释放播放器资源
 * 解除旧 MediaCodec 与 Surface 的绑定,防止新视频 configure_surface 失败
 */
public void releaseForVideoSwitch() {
    try {
        // 1. 先解除 Surface 和旧播放器的绑定
        if (getGSYVideoManager() != null) {
            getGSYVideoManager().setDisplay(null);
        }
    } catch (Exception e) {
        Log.w(TAG, "setDisplay(null) failed", e);
    }

    // 2. 清空 Surface 引用,防止 startPrepare 中的 PlayerInitSuccessListener 复用旧 Surface
    //    mSurface 为 null 时,startPrepare 会走 else 分支(Dummy codec first),
    //    等 onPrepared → startAfterPrepared → addTextureView 创建全新的 Surface
    mSurface = null;

    // 3. 清除回调并释放播放器
    setVideoAllCallBack(null);
    GSYVideoManager.releaseAllVideos();

    Log.i(TAG, "releaseForVideoSwitch 完成,旧 Surface 已清除");
}
VideoPlayerActivity.java
/**
 * 释放当前正在播放的播放器资源
 * 必须在 setUp 新视频之前调用,否则 MediaCodec 无法在同一 Surface 上重新配置
 */
private void releaseCurrentPlayer() {
    try {
        Log.i(TAG, "释放当前播放器资源");
        // 调用 HHTMediaPlayer 的专用方法:
        // 1) 解除旧 Surface 与 MediaCodec 的绑定
        // 2) 清空 mSurface 防止 startPrepare 复用旧 Surface
        // 3) 释放播放器
        videoPlayer.releaseForVideoSwitch();
    } catch (Exception e) {
        Log.e(TAG, "释放播放器资源异常: ", e);
    }
}

修复效果

修复前日志:
02-05 17:34:10.311 I IJKMEDIA: Successfully selected codec: c2.goke.hevc.decoder
02-05 17:34:10.402 E IJKMEDIA: reconfigure_codec_l:configure_surface: failed
02-05 17:34:10.402 D IJKMEDIA: SDL_AMediaCodec_decreaseReference(): ref=0

修复后日志:
02-05 18:00:00.123 I VideoPlayerActivity: 释放当前播放器资源
02-05 18:00:00.124 I HHTMediaPlayer: releaseForVideoSwitch 完成,旧 Surface 已清除
02-05 18:00:00.456 I HHTMediaPlayer:  Surface is null, will use Dummy codec first
02-05 18:00:01.789 I HHTMediaPlayer:  onSurfaceAvailable called
02-05 18:00:02.012 I IJKMEDIA: Successfully selected codec: c2.goke.hevc.decoder
02-05 18:00:02.015 I IJKMEDIA: MediaCodec configure success

调试指南

日志关键点

  1. 检查 Surface 清理
Log.i(TAG, "setDisplay(null) called");
Log.i(TAG, "mSurface = null");
  1. 检查 startPrepare 是否跳过设置
Log.i(TAG, "★ Surface is null, will use Dummy codec first");
  1. 检查新 Surface 创建
Log.i(TAG, "★ onSurfaceAvailable called");
  1. 检查 MediaCodec 配置
I IJKMEDIA: Successfully selected codec: c2.goke.hevc.decoder
I IJKMEDIA: MediaCodec configure success

常见错误模式

错误日志原因解决方法
reconfigure_codec_l:configure_surface: failedSurface 冲突调用 setDisplay(null)
SDL_AMediaCodec_decreaseReference(): ref=0MediaCodec 提前释放检查生命周期
黑屏但无错误Surface 未及时设置检查 onSurfaceAvailable
播放卡顿然后恢复MediaCodec 降级到软解检查硬解配置

调试步骤

  1. 确认问题场景

    • 第一次播放是否成功?
    • 切换视频是否失败?
    • 关闭重开是否正常?
  2. 添加日志

    Log.i(TAG, "releaseForVideoSwitch start");
    Log.i(TAG, "setDisplay(null) done");
    Log.i(TAG, "mSurface = " + mSurface);
    Log.i(TAG, "releaseAllVideos done");
    
  3. 检查 Surface 状态

    if (mSurface != null) {
        Log.w(TAG, "⚠️ mSurface should be null here!");
    }
    
  4. 验证 MediaCodec 选择

    adb logcat | grep -i "mediacodec\|ijkmedia"
    
  5. 抓取完整日志

    adb logcat -v threadtime > player_debug.log
    

性能监控

// 在 releaseForVideoSwitch 中添加
long startTime = System.currentTimeMillis();
// ... 释放操作 ...
long duration = System.currentTimeMillis() - startTime;
Log.i(TAG, "releaseForVideoSwitch took " + duration + "ms");

总结

关键要点

  1. Surface 独占原则

    • 一个 Surface 同时只能被一个 MediaCodec 占用
    • 切换视频前必须先解除旧的绑定
  2. 释放顺序

    • 正确:setDisplay(null)mSurface = nullreleaseAllVideos()
    • 错误:直接 releaseAllVideos() 没有先解绑
  3. Surface 创建时机

    • 修复后:onPrepared()addTextureView() → 系统异步创建全新 Surface
    • 修复前:复用旧 Surface,导致冲突
  4. 为什么关闭窗口没问题

    • removeAllViews() 触发 onSurfaceTextureDestroyed() 自动调用 setDisplay(null)
    • Activity 销毁,所有引用清空,下次完全隔离

最佳实践

  1. 切换视频时

    videoPlayer.releaseForVideoSwitch(); // 先完整清理
    videoPlayer.setUp(newUrl, ...);      // 再设置新视频
    videoPlayer.startPlayLogic();        // 最后开始播放
    
  2. Activity 销毁时

    videoPlayer.release(); // 使用标准 release,会触发完整清理
    
  3. 监控 Surface 状态

    if (mSurface != null) {
        Log.w(TAG, "Surface leak detected!");
    }
    

延伸阅读


文档版本:v1.0
最后更新:2026-02-09
作者:GitHub Copilot + 用户协作