自定义Extractor接入外部格式
ExoPlayer原生支持的媒体格式由内置的Extractor处理,但面对私有容器、实时流或自定义封装时,开发者必须实现自定义Extractor。本章从Extractor接口的解析流程出发,逐步引导你编写一个针对TS流的TsExtractor变体,并完成注册。最终你将掌握:如何让ExoPlayer识别并解析任意格式的容器数据。
4.1 Extractor接口解析流程
ExoPlayer的Extractor接口定义在com.google.android.exoplayer2.extractor包中,是媒体数据解析的核心抽象。一个Extractor的生命周期包含以下阶段:
- 初始化:
init(ExtractorOutput output)—— 接收输出轨道和SeekMap。 - 数据读取:
read(ExtractorInput input, PositionHolder seekPosition)—— 从输入流解析数据,返回RESULT_CONTINUE、RESULT_SEEK或RESULT_END_OF_INPUT。 - 轨道输出:通过
ExtractorOutput.track(int id, int type)获取TrackOutput,写入样本数据。 - Seek:
seek(long position, long timeUs)—— 重置解析状态。 - 释放:
release()—— 清理资源。
ExtractorOutput.endTracks()。
以下是一个典型的Extractor实现骨架(伪代码):
public class MyExtractor implements Extractor {
private ExtractorOutput output;
private TrackOutput trackOutput;
private boolean tracksEnded;
@Override
public void init(ExtractorOutput output) {
this.output = output;
// 示例:添加一条视频轨道,MIME类型为 video/raw
trackOutput = output.track(0, C.TRACK_TYPE_VIDEO);
trackOutput.format(new Format.Builder()
.setSampleMimeType("video/raw")
.setWidth(1920).setHeight(1080)
.build());
output.endTracks();
tracksEnded = true;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
// 读取数据头,判断是否可解析
if (shouldSkip(input)) return RESULT_CONTINUE;
if (isEnd(input)) return RESULT_END_OF_INPUT;
// 解析一帧数据并写入TrackOutput
int sampleSize = parseOneFrame(input);
trackOutput.sampleData(input, sampleSize, false);
trackOutput.sampleMetadata(
pts, // 时间戳
C.BUFFER_FLAG_KEY_FRAME,
sampleSize, 0, null);
return RESULT_CONTINUE;
}
@Override
public void seek(long position, long timeUs) {
// 重置内部解析缓冲区
}
@Override
public void release() { }
}
4.2 编写TsExtractor变体
TS(MPEG-2 Transport Stream)是广泛使用的直播容器格式。ExoPlayer内置了TsExtractor,但有时需要定制(例如处理私有PES流或自定义表扩展)。下面我们编写一个CustomTsExtractor,继承自TsExtractor并重写关键方法。
实现步骤:
- 继承TsExtractor,覆盖
read()或追加私有节目标识。 - 重写
init(),在父类基础上添加自定义TrackOutput。 - 处理自定义PID:在
read()中识别PAT/PMT表,注册私有流PID。
public class CustomTsExtractor extends TsExtractor {
private static final int CUSTOM_PID = 0x110;
private TrackOutput customTrackOutput;
public CustomTsExtractor(@TsExtractor.Mode int mode) {
super(mode);
}
@Override
public void init(ExtractorOutput output) {
super.init(output); // 先走父类初始化
// 添加自定义轨道
customTrackOutput = output.track(2, C.TRACK_TYPE_UNKNOWN);
customTrackOutput.format(new Format.Builder()
.setSampleMimeType("application/octet-stream")
.setId("custom")
.build());
output.endTracks();
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
// 在父类处理之前,检查是否包含自定义PID分组
int result = super.read(input, seekPosition);
// 可以在此处拦截TS包,提取私有数据
// 注意:父类read已消耗input,需谨慎操作
return result;
}
}
TsExtractor并重写read()可能会导致与父类内部状态冲突。更稳定的做法是直接实现Extractor接口,自行解析TS包。此处仅演示概念。
4.3 注册CustomExtractor
ExoPlayer通过ExtractorsFactory来实例化Extractor列表。注册自定义Extractor的方式有三种:
| 方式 | 描述 | 适用场景 |
|---|---|---|
1. 直接覆盖DefaultExtractorsFactory | 通过setExtractors()传入自定义列表 | 完全替换内置解析器 |
2. 自定义ExtractorsFactory | 实现接口并返回Extractor[] | 需要条件逻辑选择Extractor |
3. 使用ProgressiveMediaSource.Factory注入 | 通过setExtractorsFactory()添加 | 仅对某个MediaSource生效 |
最常用的方式是第一种:修改DefaultExtractorsFactory实例。
DefaultExtractorsFactory factory = new DefaultExtractorsFactory();
// 方式1:将自定义Extractor添加到列表开头(优先级最高)
factory.setExtractors(new Extractor[] {
new CustomTsExtractor(TsExtractor.MODE_MUXED), // 优先使用自定义
new Mp4Extractor(),
new TsExtractor(),
// ... 其他内置
});
// 方式2:从现有列表追加
List<Extractor> list = new ArrayList<>(Arrays.asList(factory.createExtractors()));
list.add(0, new CustomTsExtractor(TsExtractor.MODE_MUXED));
factory.setExtractors(list.toArray(new Extractor[0]));
// 使用该factory创建MediaSource
ProgressiveMediaSource mediaSource =
new ProgressiveMediaSource.Factory(factory).createMediaSource(mediaUri);
当ExoPlayer收到一个媒体文件时,会按extractors数组的顺序依次调用每个Extractor的read()方法直到某个Extractor返回非RESULT_END_OF_INPUT或投递了数据。自定义Extractor通过sniff()(如果实现ExtractorWithSniff)快速判断是否支持当前流。
4.4 调试与部署
调试自定义Extractor时,建议添加日志追踪每个步骤:
private static final String TAG = "CustomTsExtractor";
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
long position = input.getPosition();
Log.d(TAG, "read called at position: " + position);
int result = super.read(input, seekPosition);
Log.d(TAG, "read result: " + result + " bytesRead: " + input.getBytesRead());
return result;
}
部署时需注意:
- 将自定义Extractor类放在App模块中,确保ProGuard不混淆(添加
-keep class com.yourpackage.CustomTsExtractor)。 - 如果解析的数据需要额外权限(如网络元数据),在
AndroidManifest.xml中声明。 - 对于流式直播场景,建议在
read()中处理网络中断与重连。
4.5 常见错误与性能优化
| 常见错误 | 原因 | 解决方案 |
|---|---|---|
IllegalStateException: Track already ended | 在endTracks()后再次调用output.track() | 确保endTracks()只在初始化阶段调用一次 |
| 解析一直返回0字节 | 未正确读取输入或未调用trackOutput.sampleData() | 检查ExtractorInput读取方法,使用peek()预读 |
| 音画不同步 | 时间戳计算错误或未使用sampleMetadata()的duration | 基于容器时间基换算成微秒 |
| 内存溢出(OOM) | 未限制单个样本大小或缓存过多帧 | 在read()中控制读取量,实现背压机制 |
性能优化要点:
- 复用
ParsableByteArray和Buffer减少GC。 - 对于TS流,使用
TsExtractor.MODE_HLS模式提高容器对齐效率。 - 在
init()阶段预分配轨道和缓冲区大小,避免运行时动态扩容。 - 如果自定义Extractor需要处理大量数据,考虑在
read()内部使用批量解析而非逐帧。
TsExtractor并重写read()拦截自定义PID数据包,使用MODE_MUXED模式,在三星Galaxy S21上1080p 60fps流解析耗时降低12%。
内联SVG流程图
下图展示了自定义Extractor从注册到解析交付的完整流程:
ExoPlayer’s ExtractorsFactory.createExtractors()返回的列表调试定位内置Extractor已覆盖的格式,避免重复“造轮子”。