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. 关键成员变量
| 类 | 变量 | 作用 |
|---|---|---|
GSYTextureRenderView | protected Surface mSurface | 保存当前渲染使用的 Surface 引用 |
IjkPlayerManager | private Surface surface | IjkPlayer 层面的 Surface 引用 |
GSYTextureView | private Surface mSurface | TextureView 创建的 Surface 包装 |
GSYVideoManager | IPlayerManager 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 销毁,所有资源回收
关键时序:
removeAllViews()→ Android 系统触发onSurfaceTextureDestroyed()setDisplay(null)→ MediaCodec 先解除 Surface 绑定- 最后 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)
└─ ❌ 播放失败
问题链条:
releaseCurrentPlayer()只调用了GSYVideoManager.releaseAllVideos()- 没有先调用
setDisplay(null)解除 Surface 绑定 mSurface成员变量仍然保留着旧 Surface 的引用startPrepare()中PlayerInitSuccessListener捕获到了旧mSurface- 新 IjkMediaPlayer 创建后,立即被设置了旧 Surface
- 旧 MediaCodec 虽然
release()了,但 native 释放有延迟 - 新 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)
│
└─ ✅ 成功!
硬解码配置完成
开始渲染输出
成功关键:
- Step 1:
setDisplay(null)→ 旧 MediaCodec 先解除 Surface 绑定 - Step 2:
mSurface = null→ 阻止startPrepare()复用旧 Surface - Step 3:
releaseAllVideos()→ 安全释放旧播放器 - Step 4:
addTextureView()→ 创建全新 TextureView 和 Surface - 系统回调:
onSurfaceTextureAvailable()→ 全新 Surface 设置到新播放器 - 结果: 新 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 | ❌ 复用旧 Surface | ✅ addTextureView() 创建全新 |
| 新 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 绑定规则
- 一个 Surface 同时只能被一个 MediaCodec 占用
configure()时绑定,release()时解绑- 重要:
release()是异步的,native 层可能延迟释放 - 在 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 复用逻辑冲突
详细分析
-
GSYVideoPlayer 的设计假设
- 假设 Surface 的生命周期由 TextureView 管理
- 正常退出时,
removeAllViews()会触发onSurfaceTextureDestroyed()自动清理 - 但切换视频时 TextureView 并没有被移除,所以没有触发清理回调
-
HHTMediaPlayer 的优化适得其反
// HHTMediaPlayer.startPrepare() 中的优化 final Surface surfaceToSet = mSurface; if (surfaceToSet != null && surfaceToSet.isValid()) { // 提前设置 Surface 避免黑屏 GSYVideoManager.instance().setPlayerInitSuccessListener(...); }- 原意:提前设置 Surface,避免
onPrepared()后才创建 TextureView 导致的短暂黑屏 - 副作用:切换视频时,
mSurface仍是旧的,导致新播放器绑定旧 Surface
- 原意:提前设置 Surface,避免
-
IjkMediaPlayer native 层的异步性
- Java 层调用
mediaPlayer.release()立即返回 - native 层可能在另一个线程中慢慢释放资源
- MediaCodec 的
AMediaCodec_delete()也是异步的 - Goke SoC 的实现可能比高通、MTK 更慢
- Java 层调用
-
硬解码模式的 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
调试指南
日志关键点
- 检查 Surface 清理
Log.i(TAG, "setDisplay(null) called");
Log.i(TAG, "mSurface = null");
- 检查 startPrepare 是否跳过设置
Log.i(TAG, "★ Surface is null, will use Dummy codec first");
- 检查新 Surface 创建
Log.i(TAG, "★ onSurfaceAvailable called");
- 检查 MediaCodec 配置
I IJKMEDIA: Successfully selected codec: c2.goke.hevc.decoder
I IJKMEDIA: MediaCodec configure success
常见错误模式
| 错误日志 | 原因 | 解决方法 |
|---|---|---|
reconfigure_codec_l:configure_surface: failed | Surface 冲突 | 调用 setDisplay(null) |
SDL_AMediaCodec_decreaseReference(): ref=0 | MediaCodec 提前释放 | 检查生命周期 |
| 黑屏但无错误 | Surface 未及时设置 | 检查 onSurfaceAvailable |
| 播放卡顿然后恢复 | MediaCodec 降级到软解 | 检查硬解配置 |
调试步骤
-
确认问题场景
- 第一次播放是否成功?
- 切换视频是否失败?
- 关闭重开是否正常?
-
添加日志
Log.i(TAG, "releaseForVideoSwitch start"); Log.i(TAG, "setDisplay(null) done"); Log.i(TAG, "mSurface = " + mSurface); Log.i(TAG, "releaseAllVideos done"); -
检查 Surface 状态
if (mSurface != null) { Log.w(TAG, "⚠️ mSurface should be null here!"); } -
验证 MediaCodec 选择
adb logcat | grep -i "mediacodec\|ijkmedia" -
抓取完整日志
adb logcat -v threadtime > player_debug.log
性能监控
// 在 releaseForVideoSwitch 中添加
long startTime = System.currentTimeMillis();
// ... 释放操作 ...
long duration = System.currentTimeMillis() - startTime;
Log.i(TAG, "releaseForVideoSwitch took " + duration + "ms");
总结
关键要点
-
Surface 独占原则
- 一个 Surface 同时只能被一个 MediaCodec 占用
- 切换视频前必须先解除旧的绑定
-
释放顺序
- 正确:
setDisplay(null)→mSurface = null→releaseAllVideos() - 错误:直接
releaseAllVideos()没有先解绑
- 正确:
-
Surface 创建时机
- 修复后:
onPrepared()→addTextureView()→ 系统异步创建全新 Surface - 修复前:复用旧 Surface,导致冲突
- 修复后:
-
为什么关闭窗口没问题
removeAllViews()触发onSurfaceTextureDestroyed()自动调用setDisplay(null)- Activity 销毁,所有引用清空,下次完全隔离
最佳实践
-
切换视频时
videoPlayer.releaseForVideoSwitch(); // 先完整清理 videoPlayer.setUp(newUrl, ...); // 再设置新视频 videoPlayer.startPlayLogic(); // 最后开始播放 -
Activity 销毁时
videoPlayer.release(); // 使用标准 release,会触发完整清理 -
监控 Surface 状态
if (mSurface != null) { Log.w(TAG, "Surface leak detected!"); }
延伸阅读
文档版本:v1.0
最后更新:2026-02-09
作者:GitHub Copilot + 用户协作