Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Media3 视频播放:解码、帧时序与渲染

decoder(解码器)解出视频帧,离用户看见这一帧还有一整条链路:普通 Media3 播放要经过帧时序判断、releaseOutputBuffer()、输出 Surface、BufferQueue、SurfaceFlinger、HWC 和面板扫描。任一阶段延迟,都可能表现为首帧慢、画面卡住、声音继续或掉帧。

这些阶段分属不同组件,我们排查时要先说清证据指向哪一层:播放器事件解释 Media3 做了什么;Android 平台 trace 解释帧怎样到达合成器;present fence 或设备显示证据才接近“画面何时显示”。三类证据各管一段。

正文保留源码、日志和 Perfetto 中可检索的英文名,含义统一如下:

  • sample / PTS:sample 是送入解码器的压缩媒体数据;PTS(Presentation Timestamp,呈现时间戳)说明这帧在媒体时间线上的目标时刻。
  • decoder / renderer / adapter:decoder 负责解码;Media3 renderer(渲染器)推进播放状态并决定如何处理输出帧;adapter(适配器)封装同步或异步 MediaCodec 调用。
  • release timestamp / release action:release timestamp 是 renderer 请求 Surface 接收该帧的系统时间;release action 是 renderer 对当前输出帧作出的立即释放、定时释放、丢弃、跳过或稍后重试等决定。
  • BufferQueue / fence:BufferQueue 管理 Producer 与 Consumer 之间循环复用的 buffer;fence 是跨 codec、GPU 与显示硬件传递的完成信号。
  • latch / present:latch 表示 SurfaceFlinger 为某个显示周期选中并采纳一块 buffer;特定 latch-unsignaled 路径可在 acquire fence 未 signal 时先 latch,像素消费仍有同步依赖;present 表示该周期已经交给显示路径,仍早于面板像素的光学响应完成。
  • HWC 与 DEVICE / CLIENT composition:HWC(Hardware Composer,硬件合成器)按当前整屏图层集合选择合成方式。DEVICE 表示交给显示硬件处理;CLIENT 表示 RenderEngine 先用 GPU 合成中间目标。
  • drop / skip:drop 表示本应显示但因迟到而丢弃;skip 表示 decode-only(只解码、不显示)、joining(新视频流接入正在进行的播放)追赶等有意跳过。两者可能都不显示画面,统计含义不同。

一、版本基线与证据范围

核查基线如下:

层级基线关注点
Media31.11.0 稳定版 / 2bc207851df311340767e913931ca7b28cab1794renderer、codec adapter、frame release、surface、effects、事件
Android 平台Android 17 / API 37 / android-17.0.0_r1MediaCodec、CCodec/ACodec、BufferQueue、SurfaceFlinger
Android 内核android17-6.18-2026-06_r6dma-buf、dma-fence、sync_file 的共通语义

Media3 与 Android 平台独立发布。Android 17 设备可以运行较早的 Media3;升级 Media3 换不掉设备上的 vendor codec、Composer HAL、显示驱动或固件。涉及常量和默认策略时,要同时记录库版本与系统 build。

截至 2026-10-25,Google Maven 与官方发布页的最新稳定版是 Media3 1.11.1(2026-09-10 发布),另有 1.12.0-alpha01。本文机制与阈值仍固定在 1.11.0 标签;“最新版本”是另一条线索,引用时与该源码基线分开记。1.11.1 已修复 secondary renderer 预热导致的停播、seek 后 Surface 未归还主 renderer,以及 scrubbing 旧帧问题,遇到这些症状应同时核对修复版本。

系统路径同时沿用 SurfaceView、TextureView 与 video overlay 的通用结论。公开源码能给出 Android 17 侧的强证据;vendor codec、HWC plane、secure video path 和驱动等待要靠目标设备补证。

二、普通播放的分层模型

我们先看一次普通 Surface 输出涉及的主要对象。

flowchart LR
    Source["Media source
compressed samples + PTS"] --> Renderer["Media3 MediaCodecVideoRenderer"] Renderer --> Adapter["MediaCodecAdapter
sync or async"] Adapter --> API["android.media.MediaCodec"] API --> Native["Stagefright MediaCodec"] Native --> C2["CCodec / Codec2"] Native --> OMX["ACodec / OMX"] C2 --> Surface["Output Surface / BufferQueue"] OMX --> Surface Surface --> Layer["SurfaceFlinger layer"] Layer --> HWC["HWC strategy
DEVICE or CLIENT..."] HWC --> Display["Display present"] Clock["Media clock + playback position"] --> Renderer VSync["Display VSync estimate"] --> Renderer

图中的 CCodec 与 ACodec 是运行时二选一的 native codec 适配路径,看图时取其一即可。分工上,Media3 选择 codec、安排输入并决定输出帧何时交给 Surface;SurfaceFlinger 与 HWC 决定这一层怎样参与当前显示帧的合成。

Media3 负责什么

与渲染直接相关的职责包括:

  • 选择 MediaCodecInfo 与对应的 decoder;
  • 创建同步或异步 MediaCodecAdapter;
  • 把压缩 sample 和 PTS 送入 decoder;
  • 依据播放位置、倍速和 VSync 估计安排输出帧;
  • 主动 skip、drop 或追到关键帧;
  • 管理 SurfaceView、TextureView 或效果管线的输出面;
  • 上报 decoder 初始化、格式变化、首帧、掉帧和处理偏移。

Media3 的默认 ABR 不读取 HWC composition type,也不会根据 renderer 掉帧自动降低清晰度。网络选档、解码能力与显示能力之间的关联,要靠应用自己建立策略。

Android 平台负责什么

Android 侧负责:

  • MediaCodec Java API 与 native Stagefright 状态机;
  • CCodec/Codec2 或 ACodec/OMX 组件接入;
  • decoder 输出 graphic buffer 的所有权周转;
  • Surface 与 BufferQueue 的 queue、acquire、release;
  • SurfaceFlinger 的 latch、layer composition 与 HWC 提交;
  • fence 在 codec、GPU、合成器、HWC 之间传递完成状态。

releaseOutputBuffer(index, timestampNs) 返回,只说明 client 已请求把 buffer 按给定时间送往输出 Surface;BufferQueue consumer 是否已 acquire、HWC 是否已提交、面板是否已经扫描,都要另找平台侧证据。

三、Media3 的异步 codec adapter

adapter 是播放器与 codec 之间的封装层,它的线程模型决定了 trace 里会看到哪些线程。我们从默认行为看起。

API 31 及以上的默认行为

Media3 1.11.0 的 DefaultMediaCodecAdapterFactory 在 API 31 及以上默认创建 AsynchronousMediaCodecAdapter。较早系统通常创建同步 adapter,但带 Context 的默认 factory 对具有 com.amazon.hardware.tv_screen feature 的 API 28+ 设备也启用异步;无参旧构造函数没有这个 Context 检查;应用可以从 API 23 起强制启用异步模式,也可以强制关闭。

异步 adapter 包含两组不同工作:

  1. MediaCodec.Callback 在专用 callback thread 接收 input/output buffer 可用、format change 与 codec error;
  2. 常规配置中 input buffer 通过另一个 queueing thread 提交;下文 API36+ crypto-async 配置改用同步 enqueuer,独立输入排队线程只属于常规配置。

播放器的 playback thread 仍会从 adapter 的内部队列取 index,并执行 renderer 状态机;异步 callback 只把事件送进内部队列,完整播放器逻辑留在 playback thread 上。

异步回调、输入排队与playback各司其职

图:普通配置有callback与queueing线程;API36 crypto分支改同步enqueuer。

Media3 1.11.0 还默认启用 dynamic scheduling(动态调度):播放器工作循环尽量等到 renderer 可以继续推进时再唤醒,不再只按固定间隔运行。它控制的是 playback thread 的唤醒时机,与 MediaCodec.Callback 的异步 adapter 是两套机制;排查线程调度时要分别记录。

Android 17 的 crypto async 分支

Media3 1.11.0 在异步 adapter 中还处理 CONFIGURE_FLAG_USE_CRYPTO_ASYNC。DefaultMediaCodecAdapterFactory(Context) 默认启用这项实验配置,但源码只在 API 36 及以上设置 flag;此时 input buffer 改由同步 enqueuer 提交,crypto 工作交给 codec 的异步路径。它属于 secure input queueing 行为,图形输出 Surface 的异步状态要另行确认。

Android 17 MediaCodec.Callback 的回调边界

Android 17 framework 里的 buffer 回调不是直接在 codec 组件线程执行完整播放器逻辑。native MediaCodec 初始化独立的 mCodecLooper,把 CodecBase::BufferCallback 接到 kWhatCodecNotify;onInputBufferAvailable() / onOutputBufferAvailable() 分别投递 kWhatFillThisBuffer / kWhatDrainThisBuffer,Java MediaCodec.EventHandler 再按 CB_INPUT_AVAILABLE、CB_OUTPUT_AVAILABLE、CB_OUTPUT_FORMAT_CHANGE、CB_ERROR 分发到应用注册的 Callback。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §1.1、§2.1、§2.2;Android 17 MediaCodec.java / MediaCodec.cpp]

Media3 的异步 adapter 在应用侧拿到的,是 framework 整理后的 buffer index、format 与 error 事件。Perfetto 排查时我们同时标出 Media3 playback thread、adapter callback thread、adapter queueing thread,以及 framework/native codec looper,把每个等待归到所属线程;renderer 决策只是其中一类。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §2.1、§2.2]

Android 17 MediaCodec.java 还定义了 CB_LARGE_FRAME_OUTPUT_AVAILABLE、CB_REQUIRED_RESOURCES_CHANGE 等事件码;所选源码调研只核到定义,未追踪常规 Media3 视频播放的触发链路。因此它们先当排查时的版本线索用;默认播放路径的结论要等触发链路核实。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §1.1、未验证/待深入]

与 BufferQueue asyncMode 的区别

这四个机制名字相近,所在层和控制对象各不相同:

机制所在层控制什么
AsynchronousMediaCodecAdapterMedia3 / MediaCodec APIbuffer callback 与 input queueing 的线程模型
MediaCodec asynchronous modeframework codec API通过 MediaCodec.Callback 提供 buffer 可用事件
BufferQueue asyncModenative 图形队列producer queue 时是否采用可替换的 droppable slot 语义
EGL swap interval 0EGL producernative GL producer 的交换节奏,可能触发其输出 Surface 的 asyncMode

普通 PlayerView 播放使用异步 codec adapter,与 decoder 输出 BufferQueue 的 asyncMode 是两件事,对应上表的不同行。Surface::setSwapInterval(0) 只与可控制该 native EGL producer 的链路相关;默认 decoder Surface 输出不提供同名 Java 调优开关。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §4.3、§5;Android 17 Surface.cpp]

何时考虑强制配置

API 31—37 一般先保留默认值。强制切换适合受控实验:

  • 某机型出现 callback 或 flush 竞态;
  • 需要验证同步 dequeue 是否造成 playback thread 阻塞;
  • 自定义 RenderersFactory 已经改变默认 factory;
  • 旧系统需要评估 API 23+ 的异步 adapter。

这段代码用于建立同步与异步 adapter 的 A/B 组。

val renderersFactory = DefaultRenderersFactory(context)
    .forceEnableMediaCodecAsynchronousQueueing()

val player = ExoPlayer.Builder(context, renderersFactory)
    .build()

实验的另一组应调用 forceDisableMediaCodecAsynchronousQueueing(),并保持内容、Surface 类型、codec、DRM、显示模式和温度区间一致。仅比较平均首帧容易掩盖 flush、seek 与 playlist transition 的尾部延迟。

四、从 PTS 到 release timestamp

一帧什么时候交给 Surface,由 renderer 按媒体时钟和 VSync 估计逐帧计算。我们从头看这个计算。

renderer 计算的是“还早多少”

VideoFrameReleaseControl 先计算:

earlyUs = (framePresentationTimeUs - playerPositionUs) / playbackSpeed - loopElapsedUs

公式的 loopElapsedUs 只在 renderer 已 started 时扣除;实现先按 double 倍速计算后转 long,再按显示节奏调整 release time。首帧、无 Surface、decode-only、是否 started 和 joining 等分支先后参与决策;这个差值只是第一道输入,丢帧判断要沿完整决策链往下看。

正值表示该帧还早,负值表示已经晚。随后 VideoFrameReleaseHelper 根据已观测到的帧间隔、播放倍速与显示 VSync 估计调整 release time。

Media3 1.11.0 的普通输出路径包含几组重要阈值:

条件1.11.0 默认处理
帧仍早于目标超过 early scheduling threshold(默认 50 ms)TRY_AGAIN_LATER,暂不释放
earlyUs < -30_000 且非末帧可以 drop 当前输出帧
earlyUs < -500_000 且非末帧可以丢到关键帧并 flush/reinitialize codec
earlyUs < -30_000,且 elapsedSinceLastReleaseUs > 100_000,并通过 started/时间推进/joining 检查可以强制释放一帧,避免画面长时间停住
decoder input 预计晚约 15 ms 以上非末帧、无 supplemental data,并由不被依赖标记或 AV1 解析确认安全时,才可提前丢弃全部或部分输入

这些值属于 Media3 1.11.0 的 MediaCodecVideoRenderer 与 VideoFrameReleaseControl,与 Android 17 平台常量分属两个出处。1.11.0 新增 MediaCodecVideoRenderer.Builder.setEarlySchedulingThresholdUs(),也可通过 DefaultRenderersFactory.setVideoRendererEarlySchedulingThresholdUs() 调整默认 50 ms 的提前调度门槛;子类仍可覆盖部分决策,实验 API 也可以关闭 input drop 门槛。

表中条件是候选规则,同一帧可能同时命中多条,最终 action 由判断顺序决定:先判断 decode-only/Surface/强制释放,再判断更大范围追赶与 late drop,最后判断是否过早。取值正好是 -30ms、-500ms 或 100ms 时,对应严格不等式判定为假;末帧保护、子类覆盖与 joining 也会改变最终 action。

释放决策有顺序和前置条件

图:同一帧可能符合多个候选阈值,最终action由判断顺序决定。

六种 frame release action

  1. FRAME_RELEASE_IMMEDIATELY:首帧或恢复画面等场景立即释放;
  2. FRAME_RELEASE_SCHEDULED:带纳秒时间戳释放到 Surface;
  3. FRAME_RELEASE_DROP:该帧本应显示,但已经太晚;
  4. FRAME_RELEASE_SKIP:decode-only、joining 追赶等有意跳过;
  5. FRAME_RELEASE_IGNORE:可能触发更大范围的追赶动作,当前 buffer 暂不结束处理;
  6. FRAME_RELEASE_TRY_AGAIN_LATER:当前时机过早或条件未满足,下一轮再判断。

drop 与 skip 的业务语义不同。它们最终都可能调用 releaseOutputBuffer(index, false),统计却进入不同 counter。把二者合成“丢帧率”会掩盖 seek、period transition 与播放性能问题的差别。

Media3 1.11.0 调整了 joining 期间的统计:renderer 为追赶播放位置而丢弃的 buffer 计入 skipped,而不再计入 dropped。跨 1.10.1 与 1.11.0 比较掉帧指标时,应同时查看 skipped,并按库版本分桶。

timestamp 进入 BufferQueue 后会发生什么

Media3 对计划显示的帧调用:

MediaCodec.releaseOutputBuffer(index, releaseTimeNs)

CCodec 的 CCodecBufferChannel::renderOutputBuffer() 会把 graphic block 和 timestamp 交给输出 Surface。ACodec/OMX 通过自己的 port 与 native window 路径完成同类交付。

framework 的 BufferQueueConsumer::acquireBuffer() 根据 consumer 的 expectedPresent 处理队首:

  • 队首目标时间仍在未来时,可以返回 PRESENT_LATER;
  • 队列里有多帧且后一帧更适合当前 present 时,可以移除过期的前一帧;
  • 时间戳偏离 expected present 超过合理范围时,不用该异常时间戳驱动常规丢帧;
  • acquire 返回 buffer 与 acquire fence;对同步依赖的等待或 fence 传递由后续消费方处理,acquireBuffer 未必在 CPU 上阻塞到信号就绪。

源码中的 ±1 秒是 BufferQueue 判断时间戳是否合理的保护范围;把它读成 releaseOutputBuffer() 对 SurfaceView 的公开“必须相差 1 秒”约束,或播放器的掉帧门槛,都归错了层。

时间戳与fence是不同约束

图:±1秒是时间戳合理性保护范围,不是播放器晚帧门槛。

五、一次掉帧可能发生在哪一层

排查掉帧时,我们先把“没有显示新画面”按层拆开,至少有六类:

位置事件Media3 counter 能否直接看见
source / renderer inputsample 没送入 decoder,或为追赶关键帧而丢弃部分可见
decoder outputrenderer 判断帧太晚,render=false可见
Surface queueconsumer 太慢,过量帧被 Surface 丢弃通常不可完整看见
SurfaceFlinger latchacquire fence 未就绪,继续使用旧 buffer不可见
HWC composition当前层组合导致 client composition 或提交延迟不可见
display presentpresent miss 或面板继续扫描旧内容不可见

onDroppedVideoFrames() 统计的是 renderer 记录的 dropped input/output buffers;系统从解码到显示的总丢帧,还要结合表中“不可见”的那几层才能拼全。

KEY_ALLOW_FRAME_DROP 的准确语义

Android 10 起,Surface 输出默认允许在消费不及时的时候丢弃过量帧。配置格式中没有 MediaFormat.KEY_ALLOW_FRAME_DROP 时,framework 的 MediaCodec 把 mAllowFrameDroppingBySurface 设为 true;字段存在时,变量读取字段的整数值。

当值为 0 时,MediaCodec::connectToSurface() 调用 disableLegacyBufferDropPostQ(surface),选择退出该 legacy surface drop 行为。持续消费不及时会让 decoder 更容易受到背压。

使用时注意两点:

  • Android API 文档把可控退出描述为面向非 View Surface,例如独立 SurfaceTexture 或 ImageReader;
  • 跨设备“零丢帧”保证超出这个 key 的能力范围,SurfaceView 与 TextureView 的过量帧行为仍要靠目标设备验证。

Media3 1.11.0 何时写 0

普通直接输出路径不会无条件设置该 key。启用 Media3 视频效果后,renderer 有 videoSink;当 Util.isFrameDropAllowedOnSurfaceInput(context) 返回 false,也就是 API、target SDK 与机型未命中已知的不可约束条件时,Media3 才向 decoder format 写:

MediaFormat.KEY_ALLOW_FRAME_DROP = 0

原因是效果管线要在自己的输入端判断晚帧。若 decoder 输出 Surface 提前丢帧,VideoGraph 无法按媒体时间掌握完整输入。Media3 同时限制 decoder 允许积压的输出帧数,避免无限背压。

KEY_ALLOW_FRAME_DROP 决定由哪一层做帧取舍;画质增强、HWC overlay 开关归其他机制管。

六、SurfaceView、TextureView 与 raw Surface

PlayerView 默认使用 SurfaceView

Media3 1.11.0 的 PlayerView 把 surface_type 默认设为 surface_view。普通视频为什么通常优先选它?原因包括:

  • decoder 输出保持为独立 SurfaceFlinger layer;
  • HWC 可以逐帧评估视频 layer 是否适合 DEVICE composition;
  • 大面积视频有机会避免宿主 HWUI 再采样;
  • HDR、secure output 与电视端全分辨率路径通常更合适;
  • 视频与宿主 UI 可以按各自的出帧节奏工作。

独立 layer 只提供 HWC overlay 的候选条件。缩放、旋转、alpha、HDR/SDR 混合、protected usage、plane 数量、带宽和其他 layer 都会改变最终 composition type。

TextureView 增加宿主采样阶段

TextureView 路径为:

decoder → SurfaceTexture BufferQueue → app HWUI / RenderThread → App Window BufferQueue → SurfaceFlinger

它适合普通 View 级的旋转、裁剪、alpha、圆角和复杂层叠。代价包括受宿主帧截止点约束、增加 GPU 纹理采样与 App Window 提交,以及失去独立视频 layer。SurfaceFlinger 看到的是已经包含视频像素的宿主窗口。

SurfaceTexture/HWUI 可取最近输入并丢弃中间帧,所以 renderer 释放的帧还要经宿主采样,进窗口的帧数可能少于释放的帧数。SurfaceView 在现代版本也支持同步位移、一定范围的 alpha 与裁剪,“有动画”单独构不成选 TextureView 的理由;选型按实际变换、secure/HDR 与设备代价验证。

开启视频特效未必需要 TextureView:Media3 的 effects 管线可以用独立输入 Surface 和输出 Surface 完成 GPU 处理;是否选 TextureView,由最终 UI 变换需求决定。

优先交给 Player 跟踪生命周期

如果输出由 SurfaceView 或 TextureView 持有,使用:

  • player.setVideoSurfaceView(surfaceView);
  • player.setVideoTextureView(textureView);
  • PlayerView.setPlayer(player)。

这些 API 会注册 SurfaceHolder.Callback 或 SurfaceTextureListener。直接调用 setVideoSurface(surface) 时,调用方必须在 Surface 销毁前清除输出,并管理 Surface 包装对象的释放。

这段代码用于在两个 PlayerView 之间转移同一个 player。

PlayerView.switchTargetView(
    player,
    oldPlayerView,
    newPlayerView
)

该 API 会先把 player 绑定到新目标,再从旧目标解除,减少切换期间没有有效 Surface 的时间。列表复用、全屏切换和小窗转场仍要记录两侧 Surface callback 与 renderer first-frame 事件。

Player与输出Surface的生命周期

图:switchTargetView缩短无有效输出窗口,不能证明新画面立刻可见。

Android 14—17 的 Surface 生命周期

API 34 及以上,Media3 1.11.0 创建默认 SurfaceView 时把 Surface 生命周期设为 follows-attachment(跟随 View 附着状态)。暂时不可见但仍 attached 的 View 可以保留 Surface,减少销毁和重建;资源也会相应保留更久。

这项策略改善部分滚动与转场;player 生命周期仍要有明确的所有者——页面离开、列表回收、进程进入后台或业务停止播放时,按产品生命周期解绑、停止或释放 player。

Compose 中使用 PlayerView

PlayerView 放入 AndroidView 时,SurfaceView 会跨 View/Compose 与 SurfaceFlinger 的同步边界。Media3 提供 setEnableComposeSurfaceSyncWorkaround() 处理 API 34 的特定兼容问题,但该兼容方案默认关闭,因为它会影响 XML View 的 shared element transition。

只需要视频承载面时,Media3 1.11.0 的 media3-ui-compose 已提供 PlayerSurface;需要宽高比、shutter(首帧前的遮挡层)等基础容器能力时可使用 ContentFrame。PlayerSurface 仍在内部通过 AndroidView 创建 SurfaceView 或 TextureView,并负责调用 Player 的 set/clear API;decoder buffer 的终点是 Surface,变成 Compose 纹理这一步并不存在。其 SurfaceView 分支会在 API 34 自动使用 SurfaceSyncGroup 规避尺寸同步问题,API 35 及以上不走该兼容分支。

Media3 1.11.0 还修复了 ContentFrame 在播放中途重组时首帧尺寸错误的问题。这个修复只覆盖对应库内缺陷;应用自己的布局变化、Surface 重建和新尺寸首帧仍要按事件时间线验证。

Compose 页面应把以下状态分开记录:

  • composable 是否仍在 composition;
  • AndroidView 是否 attached;
  • SurfaceHolder 是否创建;
  • player 是否绑定当前 View;
  • decoder 是否仍连接原 Surface;
  • 新 Surface 的首帧是否已被 renderer 释放。

Surface 与 Compose 生命周期的详细处理见 Compose ↔ View 互操作性能实战。

七、Surface 切换与 decoder 复用

Surface 在播放中途更换时,Media3 先尝试复用现有 codec:条件满足就直接换面,满足不了再释放重建。我们看它的判断顺序。

Media3 会先判断能否更新输出面

收到新的 video output 后,MediaCodecVideoRenderer 按以下条件处理:

  1. 更新 VideoFrameReleaseControl 的目标 Surface;
  2. 若已有 codec 且无需特效 videoSink,判断 codec 是否有可用输出面;
  3. 设备不命中 setOutputSurface 兼容例外清单时调用 codec.setOutputSurface();
  4. 条件不满足时释放 codec,再走初始化;
  5. 对变更的 display Surface 重置“该输出已报告首帧”状态;仅当 renderer 为 STATE_STARTED 才 join,是否立即释放下一帧还取决于本次是否无缝换面。effects 的 videoSink 走自己的输出管理分支。

API 35 及以上,框架还提供 detached output surface 能力;Media3 只在 codec 报告支持时使用。vendor codec 的实现差异仍要逐设备验证。

generation number 解决旧 buffer 归属

framework 的 MediaCodec::connectToSurface() 为每次连接生成:

generation = (pid << 10) | (++counter & ((1 << 10) - 1))

低 10 bit 是进程内递增计数,高位来自 PID。连接时还会 disconnect/reconnect 并安装 OnBufferReleasedListener。generation number 用于防止旧连接留下的 free buffer 被错误附着到新连接。它管的是 buffer 身份归属;新 Surface 何时有内容,要看解码与渲染的进度。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §2.3;Android 17 MediaCodec.cpp]

低10bit会按1024次循环,PID也可能复用;generation是连接期的归属检查,跨进程重启、跨无限次连接的永久唯一业务ID要另行设计。日志仍需结合进程生命期、Surface身份与连接序号。

复用 codec 的格式条件

Media3 的 MediaCodecInfo.canReuseCodec() 会检查 MIME、rotation、resolution、color info、初始化数据和已知设备兼容规则。MediaCodecVideoRenderer 还检查:

  • 新分辨率是否超过初次配置的 max width/height;
  • max input size 是否超过配置范围;
  • 某些 frame-rate 变化是否要求丢弃 codec;
  • DRM session、secure decoder 与父类 renderer 状态是否允许复用。

setOutputSurface() 解决的是“一次 Surface 切换复用当前实例”;跨内容、跨 DRM session 或跨 profile 的长期共享要回到上面的格式条件判断,它当不了通用 codec 池。

playlist transition 与 prewarming

Media3 1.11.0 提供实验性的 secondary MediaCodecVideoRenderer prewarming。secondary renderer 是与当前 renderer 并存的第二个视频渲染器;prewarming(预热)让它在转场前初始化 codec 并提前处理相邻 media item。该开关默认关闭,启用后会增加 decoder、buffer、内存和功耗占用。

启用前要确认:

  • 目标设备能否同时创建两组所需 decoder;
  • secure codec 的实例数量限制;
  • 多 item 的 MIME、profile、resolution 与 DRM 组合;
  • transition 失败后的清理与回退;
  • 前台功耗和热稳定性。

常规场景先依赖 codec reuse 与准备下一段媒体;transition 尾部延迟仍达不到产品要求时,再评估 prewarming。1.11.0 新增的实验 API ExoPlayer.Builder.enablePerStreamMediaProgression(true) 允许音视频流分别向后推进,可减少部分短内容或列表转场等待;它不创建 secondary renderer,与 prewarming 要分开做 A/B 实验。secondary renderer prewarming 在1.11.0中的缺陷需结合1.11.1修复记录复测;转场停播先对照修复清单,再谈设备 decoder 容量问题。

八、Codec2/CCodec 与 OMX/ACodec

Java 层的 MediaCodec 背后有两条 native 实现路径;读 vendor codec 的 trace 前,我们先确认设备走的是哪条。

Android 17 仍保留两条实现

Stagefright 根据 codec 名称和 owner 字段(组件归属类型)创建 native codec:

  • owner 为 default 时创建 ACodec,以 codec2 开头时创建 CCodec;这个判断优先于名称;
  • 没有已识别的 owner 时,名称以 c2. 开头创建 CCodec;
  • 没有已识别的 owner 时,名称以 omx. 开头创建 ACodec。

Android17公开源码保留CCodec/Codec2与ACodec/OMX两条路径,设备实际使用哪条按codec owner/名称与日志确认。手头材料只有 ACodec 的 port 配置时,我们看到的只是两条路径之一,离完整的视频 decoder 路径还差另一半。

CCodec 的输出 Surface 路径

CCodecBufferChannel::renderOutputBuffer() 从 MediaCodecBuffer 取得 C2Buffer,读取 rotation、dataspace、HDR metadata 等信息,构造 QueueBufferInput,再调用 component 的 queueToOutputSurface()。

这条路径还请求 frame timestamp 回传。输出 Surface 直连显示时,CCodec 可以跟踪 released frame;输出是效果管线的中间 Surface 时,frame-rendered callback 可能在 buffer queue 到该中间面时就触发。此时的 callback 离 panel present 还隔着显示链路的最后几段。

命中 OMX 时的 ACodec 分析

ACodec 使用 OMX port、buffer ownership 与 native window 协调输出。排查 OMX codec 时可以看:

  • input/output port definition;
  • port settings changed;
  • OMX_FillThisBuffer 与 output ownership;
  • native window buffer 数量;
  • flush、disable/enable port 和状态转换。

命中 c2.vendor.* 时,应改看 C2Work、CCodecBufferChannel、component store 和 graphic block。两条路径的 Java MediaCodec API 相同,native trace 读法不同。

Block Model 与默认 Media3 路径

Android 17 的 MediaCodec 支持 BUFFER_MODE_BLOCK、QueueRequest 与 block model flag;这是 framework Java API 自身的能力。Media3 1.11.0 的普通 MediaCodecVideoRenderer 默认 adapter 并未把视频输入改成这条通用 block model API。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §1.2;已验证: Media3 1.11.0 AsynchronousMediaCodecAdapter.java]

异步 adapter 仍通过 getInputBuffer(index)、queueInputBuffer() 或 queueSecureInputBuffer() 工作。看到 AOSP 存在 block model 分支,还要核对 Media3 的 adapter 配置,才能谈“Block Model 零拷贝”。[来源: DeepResearch/2026-07-17-android17-media3-video-rendering-pipeline-sourcecode.md §1.2;已验证: Media3 1.11.0 AsynchronousMediaCodecAdapter.java]

九、视频效果管线

启用效果后多了一次应用侧处理

我们先看启用后的拓扑:调用 ExoPlayer.setVideoEffects() 后,Media3 通过 PlaybackVideoGraphWrapper、VideoSink 与 VideoFrameProcessor 建立处理图;应用还要包含对应的 media3-effect 依赖,否则 1.11.0 会在设置 effects 时抛出 IllegalStateException。典型拓扑为:

decoder → input Surface → VideoGraph / GL processing → display Surface → SurfaceFlinger

与直接播放相比,它增加:

  • decoder 输出到中间 Surface 的 queue/acquire;
  • effect processor 的纹理导入;
  • 一个或多个 shader pass;
  • effect 输出 Surface 的 queue;
  • 输入与输出两侧的 fence 等待;
  • 额外 graphic buffer 与 GPU 带宽。

效果是否改变分辨率、颜色空间、HDR 信息和输出节奏,要按 effect 实现与目标 Surface 验证。

效果链路必须画出输入与输出两个Surface

图:增加处理图会改变buffer、带宽、颜色与节奏,必须逐段量成本。

晚帧在效果之前处理

Media3 1.11.0 的 PlaybackVideoGraphWrapper 默认把预计晚 15 ms 以上的输入帧作为可丢候选。这个值是效果输入侧门槛,与普通 renderer 的输出晚 30 ms 门槛属于两个位置。

其实际输入决策还会先预测 early time 并校验对应输入路径;15ms是可配置策略值,效果帧的实际去留由完整决策给出,显示截止时间另有定义。

1.11.0 还修复了效果管线在 flush 时未清除 redraw 标记(待重绘状态),可能导致 seek 后播放器停住的问题。升级后仍出现同类现象时,用 input/output Surface 与 GPU 时间线确认是否是新的等待点;旧缺陷清单只能排除已知问题。

效果很重时,帧可能在以下任一点迟到:

  1. decoder output 晚;
  2. input Surface acquire fence 晚;
  3. shader pass 超过预算;
  4. output Surface queue 晚;
  5. SurfaceFlinger/HWC 没赶上目标 present。

只看 renderer dropped counter,定位 shader 或输出 Surface 的等待还不够,要沿上面这几个迟到点查。

ANGLE 命中的确认

Media3 的效果组件常以 OpenGL ES 为接口。设备可能使用 vendor GLES driver,也可能通过 ANGLE 落到 Vulkan。是否命中 ANGLE,应从同一播放器进程的 GL_RENDERER 与已加载动态库确认。

命中 ANGLE 后,滤镜冷启动可以包含:

  • GLSL 到 SPIR-V 的翻译;
  • Vulkan pipeline 创建与缓存未命中;
  • EGL native fence 导入 Vulkan semaphore;
  • ANGLE GPU thread 上的 submit 与 wait。

未命中 ANGLE 时,TranslatorSPIRV、Vulkan semaphore、ANGLE shader 缓存这些 ANGLE 侧原因可以先从列表里划掉。驱动切换也要以进程冷启动为实验单位,已有 EGL 环境不会在播放中热切换后端。

效果预热要测总成本

预热常用 shader 可以降低第一次启用滤镜的峰值,但会把编译、pipeline 创建、buffer 分配和 GPU 活动移到更早时刻。评估项应同时包含:

  • 页面首次可交互时间;
  • 播放首帧;
  • 第一次开启效果的延迟;
  • GPU memory 与 graphic buffer 峰值;
  • 后台预热的功耗和温升;
  • 缓存在进程重启、驱动更新后的命中率。

十、HDR、DRM 与 protected content

HDR 是一组连续约束

我们先把 HDR 播放涉及的环节排一遍,至少有:

  • 容器与 bitstream 的 color metadata;
  • decoder 对 MIME、profile、bit depth 的支持;
  • output graphic buffer 的 dataspace 与 HDR metadata;
  • Surface 类型和 protected usage;
  • SurfaceFlinger 的色彩管理;
  • HWC、显示器与当前 display mode;
  • UI 与视频混合时的 client/device composition 选择。

Media3 1.11.0 能在支持的 codec 上通过 PARAMETER_KEY_HDR10_PLUS_INFO 传递 HDR10+ out-of-band metadata(带外元数据)。传递只解决元数据到位;当前帧能否拿到 HWC overlay、外接显示链路能否保持动态元数据,要再看合成与显示侧。

secure decoder 与 secure Surface 要成组验证

DRM 内容可能要求 secure decoder、protected graphic buffer、secure Surface 与受保护的 HWC 路径。普通应用的 GPU 不能任意读取 protected 视频纹理,因此以下组合必须在目标设备核查:

  • secure decoder + SurfaceView;
  • secure decoder + TextureView;
  • secure decoder + Media3 effects;
  • secure HDR + tunnel;
  • secure playback + screenshot、casting 或外接显示。

遇到黑屏时要记录 DRM security level、codec 名、secure 后缀、Surface secure 状态、HDCP 与显示路由;decoder 初始化成功只是起点,这些字段补齐后证据链才完整。

SurfaceView 与 HDR overlay

SurfaceView 让视频保持独立 layer,HWC 每个 display frame 仍会重新决定 composition type。圆角、alpha、旋转、颜色转换、其他 protected layer、plane 资源与显示带宽都可能使视频回到 CLIENT composition,或触发专用 vendor 路径。

十一、tunneled playback

tunnel 是另一种帧交付模型

普通播放由应用侧 renderer 按帧调用 releaseOutputBuffer(timestampNs)。tunneled playback(隧道播放,简称 tunnel)把视频 decoder 与 AudioTrack 的硬件同步关系交给 codec/HAL,并通过 sideband handle(旁带句柄)连接视频 layer;这条旁带路径上,应用无需逐帧提交普通图像 buffer。

典型条件包括:

  • 音频与视频 renderer 都报告 tunneling capability;
  • 有效的 audio session id;
  • codec 支持所选 MIME、secure、HDR、resolution 与 frame rate 组合;
  • 输出 Surface 和设备 HWC 支持该路径。

Media3 的 setTunnelingEnabled(true) 只是 track selection 偏好。条件不满足时仍可能选择普通 renderer。

这段代码用于请求 tunnel,并保留不满足能力时的普通播放选择。

val trackSelector = DefaultTrackSelector(context).apply {
    parameters = buildUponParameters()
        .setTunnelingEnabled(true)
        .build()
}

val player = ExoPlayer.Builder(context)
    .setTrackSelector(trackSelector)
    .build()

上线前应按 codec、DRM、HDR、音频 route、倍速和显示设备建立允许列表。Media3 1.11.0 修复了一项 audio session id 竞态:应用调用 setAudioSessionId() 后,初始自动生成的 ID 不应覆盖应用设置值。使用更早版本时要核对实际 ID 与对应发布说明。

sideband 仍由 SurfaceFlinger 管理

tunnel 仍要经过 SurfaceFlinger。Surface 的 sideband stream(旁带流)仍对应一个 layer,SurfaceFlinger 管理其几何、层级和合成;HWC 按硬件 A/V sync(音视频硬件同步)获取视频内容。

tunnel经sideband连接系统图层

图:tunnel不依赖应用逐帧普通图像queue作为主证据,但仍受SF图层管理。

因此,Perfetto 中缺少普通逐帧 BufferQueue transaction,在 tunnel 下可能是正常表现。黑屏调查要改看:

  • audio session 与 AudioTrack 状态;
  • feature-tunneled-playback 配置;
  • audio-hw-sync;
  • sideband handle 是否绑定;
  • tunnel peek 与 first-tunnel-frame-ready;
  • vendor codec/HWC 日志。

tunnel callback 的语义

OnFirstTunnelFrameReadyListener 表示第一帧已解码并达到 tunnel render 条件。OnFrameRenderedListener 表示 codec/HAL 上报 frame rendered。暂停态 tunnel peek、HWC 提交和面板扫描仍可能在它们之后。

这是框架codec事件语义;系统提供该listener,默认Media3是否为应用暴露并注册它还要核对。1.11.0的tunnel renderer使用自己的frame-rendered listener推进状态,诊断需核对实际注册者与事件来源。

产品首帧指标应把“播放器首帧事件”与“用户可见首帧”命名为两个指标,避免把 callback 时间当作显示完成。

更完整的 tunnel、Codec2 与 ABR 内容见 多媒体播放管线:Codec2、Tunneled Playback 与 Media3 ABR。

十二、帧率匹配与倍速

Media3 会向 Surface 声明播放帧率

API 30 及以上,VideoFrameReleaseHelper 根据 format frame rate 或帧时间估计值计算 media frame rate,再乘播放倍速,调用 Surface.setFrameRate()。

Media3 1.11.0 的默认 videoChangeFrameRateStrategy 是 VIDEO_CHANGE_FRAME_RATE_STRATEGY_ONLY_IF_SEAMLESS。传入非零帧率时使用 FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;停止渲染或 Surface 更换时会清除旧声明。

TextureView路径声明作用于输入Surface时,独立SF视频layer并不会因此自动出现;实际显示投票还需核查宿主窗口/TextureView向系统传播的路径。倍速变化、估计器收敛和Surface变更都需确认旧声明已清除或更新。

这只是给系统的 mode selection 提示。系统还会综合:

  • 用户刷新率设置;
  • 其他可见窗口与 layer 的 vote;
  • mode 切换是否允许黑屏;
  • 电源、热与亮度约束;
  • 触控和动画的高刷新率需求;
  • 外接显示支持的模式。

出帧节奏与刷新率的配合

24 fps 视频可以在 48、72、96 或 120 Hz 上以整数次数重复显示,也可能在 60 Hz 上采用不均匀的重复节奏。SurfaceView 的独立 layer 允许视频 buffer 低频更新,宿主控制条继续以更高刷新率绘制。

TextureView 的视频内容进入宿主应用窗口后,视频更新要赶上宿主帧。即使媒体帧率声明正确,宿主 RenderThread 迟到也会影响可见出帧节奏。

倍速同时改变多个预算

2 倍速播放会使目标播放帧率与 codec operating rate 提高,网络消耗速度也增加。排查时同时观察:

  • KEY_OPERATING_RATE 与 codec 能力;
  • AudioTrack 或音频处理路径;
  • renderer processing offset;
  • Surface frame-rate vote;
  • decoder output 与 HWC present;
  • thermal throttling。

decoder 在目标倍速下处理不足时,显示刷新率提高也补不上这块短板。

刷新率选择的系统细节见 Android 17 DisplayMode 与刷新率选择。

十三、首帧、seek 与转场

首帧至少拆成五个阶段

阶段建议时间点
数据可用manifest/segment 请求完成、首个 sample 入队
decoder readycreate/configure/start 完成
output ready第一个目标 PTS 的 output buffer 可处理
renderer releasefirst frame 调用 render=true 或 timestamp release
display presentlayer latch、HWC present 或设备可见性证据

Media3 的 onRenderedFirstFrame() 普通直接输出在 renderer 释放首帧后上报;tunnel/effects等由各自事件路径推进。1.11.0 源码中的 event dispatcher 仍留有 TODO:回调时间使用派发时的 elapsedRealtime(),对应的是派发时刻,display present time 要另取显示侧。

重新设置同一个非空输出时,setOutput() 还能重新通知已报告的first-frame事件;事件次数与新的物理首帧次数是两个量,指标要按会话、导航/内容、Surface身份和状态去重。

joining 的 5 秒窗口

DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS 为 5000 ms。joining 指新视频流接入正在进行的播放;在这段时间里,renderer 可以短暂报告 ready,避免新视频尚未出帧时播放器进入中间 buffering 状态。

这个窗口服务于状态连续性;首帧 SLA 另用独立计时与报警,5 秒内画面是否可见以显示侧为准。

seek 后要识别 decode-only 与追帧

我们 seek 时,目标常落在关键帧之后。decoder 需要从前一个同步帧开始解码,目标前的 output 被标记为 decode-only 或 skip。若播放位置已经落后很多,renderer 还可能丢到后续关键帧并 flush codec。

seek 性能需要记录:

  • 请求目标时间;
  • 实际选中的同步 sample;
  • flush 或 reinitialize;
  • queued input 数;
  • skipped input/output 数;
  • dropped-to-keyframe 次数;
  • seek 后 renderer first frame;
  • seek 后 display present。

只统计 seekTo() 到 player ready,看不到用户盯着旧画面的时长;这段时长要用列表最后两项补齐:seek 后 renderer first frame 与 seek 后 display present。

黑屏与旧帧的策略取舍

PlayerView 的 shutter、keep_content_on_player_reset、旧 Surface 的 last buffer 和新 Surface 首帧共同决定转场画面。保留旧帧可以减少黑屏,也可能在内容已经切换后显示过期画面。

产品应明确:

  • 内容切换时允许保留旧帧多久;
  • DRM/用户切换时是否必须立即遮挡;
  • 新 Surface 未出帧时显示 poster、黑色还是旧画面;
  • first-frame callback 到来后何时移除遮罩。

十四、播放器侧观测

AnalyticsListener 能采到什么

这段监听代码用于采集 renderer 级事件,并保留 processing offset(帧相对目标时刻的处理偏移)的微秒总和与帧数,供加权聚合;videoLog 是业务接口。

player.addAnalyticsListener(object : AnalyticsListener {
    override fun onVideoDecoderInitialized(
        eventTime: AnalyticsListener.EventTime,
        decoderName: String,
        initializedTimestampMs: Long,
        initializationDurationMs: Long
    ) {
        videoLog.decoderInitialized(
            decoderName,
            initializedTimestampMs,
            initializationDurationMs
        )
    }

    override fun onVideoInputFormatChanged(
        eventTime: AnalyticsListener.EventTime,
        format: Format,
        decoderReuseEvaluation: DecoderReuseEvaluation?
    ) {
        videoLog.formatChanged(format, decoderReuseEvaluation)
    }

    override fun onDroppedVideoFrames(
        eventTime: AnalyticsListener.EventTime,
        droppedFrames: Int,
        elapsedMs: Long
    ) {
        videoLog.rendererDropped(droppedFrames, elapsedMs)
    }

    override fun onVideoFrameProcessingOffset(
        eventTime: AnalyticsListener.EventTime,
        totalProcessingOffsetUs: Long,
        frameCount: Int
    ) {
        if (frameCount > 0) {
            videoLog.processingOffsetBatch(totalProcessingOffsetUs, frameCount)
        }
    }

    override fun onRenderedFirstFrame(
        eventTime: AnalyticsListener.EventTime,
        output: Any,
        renderTimeMs: Long
    ) {
        videoLog.rendererFirstFrame(renderTimeMs, output.javaClass.name)
    }

    override fun onVideoCodecError(
        eventTime: AnalyticsListener.EventTime,
        videoCodecError: Exception
    ) {
        videoLog.codecError(videoCodecError)
    }
})

这些回调应带同一个 playback session id,并与 SystemClock.elapsedRealtimeNanos() 或 Perfetto 可对齐的单调时间配对。日志里的 onRenderedFirstFrame 命名为 renderer first frame;display first frame 这个名字留给显示侧指标。

PTS是媒体时间(微秒);计划release使用nanoTime()时基(纳秒);事件中的renderTimeMs使用elapsedRealtime()(毫秒)。这些时基和休眠语义不同,直接乘1000相减会算错;与Perfetto关联需记录时基映射/同步点。offset正值表示提前、负值表示迟到;平均值应为各批total之和除以各批count之和,逐帧P95要回原始批次数据取。监听回调要轻量有界,owner结束时移除listener。

DecoderCounters 怎么读

DecoderCounters 中常用字段包括:

  • decoderInitCount / decoderReleaseCount:是否频繁重建;
  • queuedInputBufferCount:输入推进量;
  • skippedInputBufferCount / skippedOutputBufferCount:有意跳过;
  • renderedOutputBufferCount:renderer 已释放为显示;
  • droppedBufferCount:renderer 因迟到丢弃;
  • droppedInputBufferCount:进入 decoder 前的晚帧;
  • maxConsecutiveDroppedBufferCount:连续卡顿强度;
  • droppedToKeyframeCount:严重追帧次数;
  • totalVideoFrameProcessingOffsetUs / count:处理提前量。

这些 counter 为调试用途,更新线程写后与读取线程读前都按契约调用 ensureUpdated();其同步屏障提供可见性,各字段各自更新,读到的并不是一次统一的事务快照。线上指标更适合通过 AnalyticsListener 聚合,避免任意线程持续轮询可变对象。 droppedBufferCount已包含droppedInputBufferCount,总掉帧直接读前者。

每个 session 至少记录什么

  • app version、Media3 version、Android build fingerprint;
  • 内容 ID、container、MIME、codec string、profile/level;
  • 分辨率、bitrate、frame rate、HDR format、DRM;
  • codec name、CCodec/ACodec、secure、tunnel;
  • SurfaceView/TextureView/effects、Surface identity;
  • display mode、requested frame rate、播放倍速;
  • decoder init/release、format reuse evaluation;
  • renderer first frame、业务可见首帧;
  • dropped/skipped/processing offset;
  • rebuffer、seek、track switch、route switch;
  • 设备温度、CPU/GPU 频率与电源模式。

缺少这些字段时,多设备聚合很容易把网络、codec、Surface 生命周期和显示问题混在同一分布里。

十五、Perfetto 取证

采集目标

我们做一次可复现采样时,trace 建议覆盖:

  • sched、CPU frequency、idle;
  • app 主线程、playback thread、codec callback/queueing thread;
  • amedia / codec 相关 trace;
  • BufferQueue、gfx、view;
  • SurfaceFlinger、FrameTimeline;
  • HWC 与 display 相关 vendor track;
  • GPU frequency、memory 与 fence wait;
  • AudioTrack/AAudio 或 tunnel 所需音频 track。

播放器自己的事件应写入 trace marker,至少标出 prepare、first input、codec initialized、output available、release、first-frame callback、seek 和 Surface callback。

普通 SurfaceView 的追帧顺序

我们按同一媒体 PTS 串起整条链路查:

PTS、codec output index、Surface frame number与SF frame token是四套ID;实际关联靠应用标记、元数据或平台可用的映射,时间接近只算线索。fence signal与latch可因latch-unsignaled策略重叠,以下是取证项目,串行时序以设备实际表现为准。

  1. input sample 何时进入 decoder;
  2. output 何时可处理;
  3. renderer 选择 drop、skip 或 release;
  4. releaseOutputBuffer 何时执行;
  5. 对应 Surface queue 是否出现;
  6. acquire fence 何时 signal;
  7. SurfaceFlinger 是否 latch 新 buffer;
  8. 当前 display frame 采用哪种 composition;
  9. present fence 何时 signal。

若 renderer 已按时 release,而 acquire fence 晚,原因偏向 decoder 输出完成或 buffer 同步。若 fence 已就绪而 SF 没 latch,要查 expected present、layer transaction 与 consumer 节奏。若 SF/HWC 已提交仍出现可见异常,再查 display、面板与采集证据。

TextureView 要增加宿主阶段

还需检查:

  1. SurfaceTexture frame available;
  2. TextureView 是否触发宿主 frame;
  3. RenderThread 何时 acquire 与采样;
  4. App Window buffer 何时 queue;
  5. 宿主 layer 是否赶上 SurfaceFlinger latch。

视频 input queue 很顺畅,宿主主线程或 RenderThread 迟到时,用户仍会看到卡顿。

effects 要画两套 Surface

给 decoder 的输出 / VideoGraph 的输入 Surface,以及 VideoGraph 的输出 / 显示 Surface 分别命名。trace 中分开计算:

  • decoder release → effect input acquire;
  • effect GPU start/end;
  • effect output queue;
  • output acquire fence;
  • SF latch → present。

ANGLE 进入 GPU backend 分析的前提,是同进程 GL_RENDERER / 后端库指纹证明命中。

tunnel 要换证据

tunnel 不依赖普通逐帧 queueBuffer() 作为主证据。采样重点转向:

  • codec input PTS;
  • AudioTrack timestamp 与 HW sync;
  • sideband layer 状态;
  • first-tunnel-frame-ready / frame-rendered;
  • HWC/vendor tunnel track;
  • seek、pause、peek、route change。

缺少 vendor trace 时,结论标为“framework 已完成配置,设备侧逐帧时序不可见”即可,display 时间等证据到位再补。

十六、常见症状的排查路线

首帧慢

依次检查:

  1. 首个可解码 sample 是否晚到;
  2. codec create/configure/start 哪段耗时;
  3. secure session 是否等待;
  4. 首个 output 是否晚;
  5. renderer 是否等待 start、Surface 或 release time;
  6. Surface 是否在中途销毁/重建;
  7. acquire fence 与 SF latch 是否晚;
  8. effects 是否发生 shader cold compile;
  9. tunnel 是否停在 first-frame-ready 或 peek。

首帧问题仅凭 player STATE_READY 分类会漏掉瓶颈段。

声音继续、画面停住

我们先判断画面停住时 renderer 是否持续 release:

  • renderer 不再拿到 output:查 decoder、input starvation、resource error;
  • renderer 持续 drop:查播放位置、解码吞吐、倍速和追帧;
  • renderer 持续 release,Surface queue 不动:查 output Surface 与 codec 连接;
  • queue 正常,fence 晚:查 codec/GPU 同步;
  • SF 持续复用旧 buffer:查 readiness、latch、HWC;
  • tunnel 模式:查 audio HW sync、sideband 与 vendor 路径。

seek 后黑屏

核对:

  • seek 前后 Surface identity;
  • codec flush 或 reinitialize;
  • 目标前 decode-only frame 数;
  • output Surface 是否仍 valid;
  • renderer first-frame 状态;
  • shutter 是否移除过早;
  • secure/tunnel 状态是否重新配置。

频繁重复 seek 时,要观察异步 callback 队列在 flush 后是否丢弃旧 callback。Media3 1.11.0 的 AsynchronousMediaCodecAdapter.flush() 按 buffer enqueuer、codec、callback 队列、codec restart 的顺序清理。

只有滤镜开启时掉帧

固定同一 decoder 与 Surface,对比:

  • effects off;
  • 单个轻量 shader;
  • 完整效果组;
  • vendor GLES;
  • ANGLE(目标设备允许时)。

记录 GPU time、input/output Surface fence、分辨率、颜色格式和效果输入 drop。若 shader 时间正常而 input acquire 很晚,问题可能仍在 decoder;若 output queue 后才迟到,应转向宿主 Surface 或显示链路。

HDR 或 DRM 设备分化

按组合建表,机型只是其中一个维度:

  • codec + secure;
  • HDR format + bit depth;
  • Surface type;
  • effects/tunnel;
  • display route;
  • composition type;
  • Widevine/HDCP 状态。

相同机型在内屏、HDMI、投屏和不同 DRM session 下可能走不同路径。

列表滑动时黑帧

检查 View 是否 attached、Surface 是否保留、player 是否被复用到错误 cell,以及旧 codec 是否仍连接旧 generation。使用 switchTargetView() 或明确的 detach/attach 协议,避免多个 cell 同时争用同一个 player output。

十七、优化优先级

低风险项

  • 使用稳定的 Media3 版本并记录准确版本;
  • 普通长视频保留 PlayerView 默认 SurfaceView;
  • 使用生命周期感知的 Surface API;
  • 日志区分 dropped、skipped、Surface drop 与 display miss;
  • 对 codec、DRM、HDR、Surface、display mode 分桶;
  • 把 renderer 事件写进 Perfetto 单调时间轴;
  • 在目标设备上证明 HWC composition,不根据控件名称猜测。

需要设备覆盖验证的项

  • 强制同步或异步 codec adapter;
  • decoder fallback;
  • setOutputSurface() 跨 View/Surface 切换;
  • tunnel allowlist;
  • effects、ANGLE 与 shader 预热;
  • experimental renderer prewarming;
  • 自定义 late-frame threshold;
  • HDR/secure/effects 组合;
  • 非无缝 display mode 切换。

不应作为通用建议的项

  • 给 PlayerView 设置 native swapInterval(0);
  • 假设 SurfaceView 固定三缓冲;
  • 假设 BufferQueue 初始 slot 数就是运行时 decoder 队列深度;
  • 用 ACodec/OMX 解释所有 Android 17 codec;
  • 宣称 Media3 默认使用 BUFFER_MODE_BLOCK;
  • 把 onRenderedFirstFrame() 当作面板显示完成;
  • 把 SurfaceView 等同于 HWC overlay;
  • 为避免掉帧而无条件把 KEY_ALLOW_FRAME_DROP 设为 0;
  • 在不同 DRM、profile 或 resolution 之间无条件池化 decoder。

十八、核查清单

版本与输入

  • 记录 Media3 与 Android build;
  • 记录 codec name、MIME、profile/level;
  • 记录 resolution、frame rate、HDR、DRM、tunnel;
  • 记录播放倍速、display mode 与 route。

Surface

  • 确认 SurfaceView、TextureView 或 effects;
  • 确认 Surface 所有者与生命周期回调;
  • 确认 Surface identity、validity 与 generation 切换;
  • Compose 场景确认 attachment 与 Surface 状态。

renderer

  • 区分 sync/async adapter;
  • 记录 decoder init/release 与 reuse evaluation;
  • 分开 dropped、skipped、dropped-to-keyframe;
  • 记录 processing offset 与 first-frame event。

系统显示

  • 对齐 output PTS、release timestamp 与 Surface queue;
  • 检查 acquire/release/present fence;
  • 检查 SF latch 与 HWC composition;
  • 将 renderer first frame 与 display evidence 分开。

特殊路径

  • effects 记录 input/output 两套 Surface;
  • ANGLE 由运行时 renderer 指纹确认;
  • tunnel 记录 audio session、HW sync 与 sideband;
  • protected content 记录 secure Surface 与 display route。

全文小结

回到全文的线索:Media3 1.11.0 在 API 31+ 默认采用异步 codec adapter,并由 MediaCodecVideoRenderer 根据媒体时钟和 VSync 安排输出。这个异步 adapter、BufferQueue asyncMode 与 EGL swap interval 是三套机制,诊断时分开记录;1.11.0 默认启用的动态调度又是播放器工作循环的第四个维度。

普通帧从 releaseOutputBuffer(timestampNs) 到可见画面,还要经过 Surface queue、fence、SurfaceFlinger、HWC 与 display present。renderer first-frame 和 dropped-frame 事件只覆盖播放器侧边界;端到端结论需要平台 trace 与显示证据。

SurfaceView 给视频独立 layer 与 HWC 评估机会,TextureView 把视频采样进宿主窗口,effects 又增加一套输入/输出 Surface 和 GPU 处理。HDR、DRM、tunnel、ANGLE 与 vendor codec 都会改变路径。稳定的优化来自版本固定、责任分层、同一时间轴取证和目标设备组合测试。

相关章节

源码与官方资料

Media3 1.11.0

历史参考:Media3 1.10.1

Android 17 / API 37

Kernel android17-6.18-2026-06_r6