如何实现 ExoPlayer 的自定义 Renderers 以支持特殊编码格式?
解读
在国内面试中,这道题常被用来区分“会用 ExoPlayer”与“吃透 ExoPlayer”。面试官想确认:
- 你是否清楚 Renderer 在 ExoPlayer 流水线中的位置与职责;
- 能否把“解码→渲染”这条链路上的 MediaCodec、AudioTrack、Surface、OpenGL、ByteBuffer、DRM、音视频同步等关键点串起来;
- 是否具备“把私有码流喂给硬件/软件解码器,再把裸数据送到对应渲染器”的落地经验,尤其是国产芯片(海思、展锐、瑞芯微)和国产 DRM(国密 SM4、商密 AVS3)场景下的适配能力;
- 能否讲清“如何不改动 ExoPlayer 主干代码,用 Renderer 工厂注入”的优雅姿势,避免“魔改源码”带来的后续升级灾难。
知识点
- ExoPlayer 整体架构:MediaSource→Load→SampleQueue→Renderer→Output;
- Renderer 接口核心方法:supportsFormat、createRenderers、render、isReady、isEnded、getState;
- 解码链路:MediaCodecAdapter、DecoderInputBuffer、SimpleOutputBuffer、AudioProcessor、VideoFrameProcessor;
- 渲染链路:AudioTrack.write、AudioTimestamp、Surface.unlockCanvasAndPost、SurfaceTexture.updateTexImage、EGL14.eglSwapBuffers;
- 私有编码格式识别:自定义 Extractor 在 sniff 阶段写入 Format.codecs = "my_private/xx" 并携带 csdb(Codec-Specific-Data-Bytes);
- 国产芯片解码器别名:OMX.hisi.video.decoder.avs3、OMX.unisoc.audio.decoder.smv;
- 国密 DRM:MediaCrypto 适配 Huawei HiTrust、Teegris TA,走 HDCP 2.3 链路;
- 音视频同步:AudioTrack.getPlaybackHeadPosition 与 VideoFrame.getPresentationTimeUs 差值驱动 MediaClock;
- 性能调优:16 ms 帧率、0 音频漂移、50 ms 首帧、< 200 ms 频道切换;
- 版本升级:RendererFactory 注入 + 独立 module,保证后续升级 ExoPlayer 2.x→3.x 零 merge 冲突。
答案
步骤一:声明私有 MIME
在自定义 Extractor 里把 Format 构造为
Format.createVideoSampleMime("video/avsx", MimeTypes.VIDEO_UNKNOWN, ...)
同步把 CodecSpecificData 塞进 Format.initializationData,方便后续 MediaCodec 配置 csd-0/csd-1。
步骤二:创建 RendererFactory
public class AVSXRendererFactory implements RendererFactory {
@Override
public Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoListener, AudioRendererEventListener audioListener, TextOutput textOutput, MetadataOutput metadataOutput) {
return new Renderer[]{
new AVSXVideoRenderer(eventHandler, videoListener,
new DefaultMediaCodecAdapter.Factory(),
MediaCodecSelector.DEFAULT,
DefaultVideoSink.Factory.newInstance(context, true),
MAX_DROPPED_FRAME_COUNT,
MAX_JOIN_TIME_MS,
false,
context.getMainExecutor())
};
}
}
步骤三:实现 AVSXVideoRenderer
继承 MediaCodecVideoRenderer,重写 supportsFormat:
@Override
protected boolean supportsFormat(MediaCodecAdapter.Factory codecFactory, Format format) {
return "video/avsx".equals(format.sampleMimeType);
}
在 configureCodec 阶段,把 ByteBuffer 中的私有 SPS/PPS 按芯片手册拼成 csd-0,调用 MediaFormat.setByteBuffer("csd-0", csd0)。
若芯片仅支持 secure decoder,则设置 MediaFormat.setFeatureEnabled("secure-playback", true),并确保 Surface 来自 SurfaceView 且 flag 为 SECURE。
步骤四:处理渲染同步
重写 processOutputBuffer,在 releaseOutputBuffer 前把 pts 与 MediaClock 对齐,若国产芯片返回的 pts 是 90 kHz 时钟,需要乘以 1000 / 90 转成微秒。
步骤五:注入工厂
ExoPlayer.Builder(context)
.setRenderersFactory(new AVSXRendererFactory())
.build()
步骤六:灰度与验证
- 使用 Systrace 确认 render 线程每帧耗时 < 12 ms,留有 4 ms 缓冲;
- 用 Battery Historian 验证 1080p60 连续播放 30 min 耗电 < 6%;
- 用 adb shell am broadcast 模拟插拔 HDMI HDCP 2.3 授权,确保 secure decoder 不崩溃;
- 在华为、小米、OPPO 三家国产 ROM 上跑 monkey 2000 次,无 native crash。
拓展思考
- 如果芯片只提供裸 YUV 输出而非 Surface,需要把 AVSXVideoRenderer 降级为 SimpleDecoderVideoRenderer,自己接管 EGL 渲染,并引入 GPU 后处理(HDR→SDR、旋转 90°)时,如何与 Jetpack Compose 的 AndroidView 无缝集成?
- 在车载场景下,仪表盘与中控屏双屏异显,要求同一码流零延迟双路输出,是否可以在一个 Renderer 里同时创建两个 VideoSink,分别喂给两个 Surface,还是必须拆成两个 ExoPlayer 实例?
- 国密 AVS3 音频层采用 SM4-GCM 加密,解码后 PCM 为 32 bit float 192 kHz,而车载功放仅支持 48 kHz 16 bit,如何在不阻塞 AudioTrack 写入线程的前提下,把 Resampling + Downmix 放到 FFmpeg AVAudioFifo 异步线程,并保证 drift < 10 ms?
- 当 Android 14 引入 Ultra HDR 10 bit 显示管线,而私有码流是 12 bit AVSX,若 GPU 不支持 12 bit OpenGL texture,是否考虑把 12 bit 量化到 10 bit 并在 GPU shader 做 inverse tone mapping,还是直接走 R8G8B8A8 高精度浮点 FBO?
- 未来 ExoPlayer 3.x 计划把 Renderer 改为 Kotlin Coroutine 协程通道,如何把现有 C++ 解码适配层封装成 Flow<VideoFrame>,并保证线程安全零拷贝?