1 简介
ExoPlayer是android非常流行的开源播放器框架,它以其出色的兼容性,在很大程度上已取代了android系统的媒体播放器,成为媒体类应用软件的标配。 初次使用ExoPlayer,涉及的类比较多,会觉得使用比较复杂;但实际上,ExoPlayer并不是给初级用户使用的,它提供了更高级的定制能力,可以支持比Android原生库更高级的媒体支持能力;甚至,用户还可以自己定义插件,支持不同的媒体格式。 更神奇的地方在于,Exoplayer这些组件并不是一整个的库,而是提供了不同的maven坐标,供客户端组合使用, 如HLS、dash、rtsp, 甚至还提供了一些依赖三方库的插件,如okhttp、ffmpeg、av1、vp9。 这篇文章并不是Exoplayer的使用文档,而是通过分析ExoPlayer的工程组织和扩展机制,了解其中组件化的思想和插件机制的奥妙。
2 基本知识
先看一段Exoplayer中的使用样例源码,涉及了很多类
mediaItems = createMediaItems(intent);
if (mediaItems.isEmpty()) {
return false;
}
lastSeenTracks = Tracks.EMPTY;
ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(/* context= */ this)
.setMediaSourceFactory(createMediaSourceFactory());
setRenderersFactory(
playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false));
player = playerBuilder.build();
player.setTrackSelectionParameters(trackSelectionParameters);
player.addListener(new PlayerEventListener());
player.addAnalyticsListener(new EventLogger());
涉及到的一些重要的单元或类:
- MediaItem: 播放的单元,一般是一个文件或url
- Track: 轨道,如视频轨道,音频轨道,字幕等; 音频可能有多个,可以选择不同的语言; 字幕也有可能有多个,可以选择不同的语言的字幕
- MediaSource: 代表可以被ExoPlayer播放的媒体,一般关联TimeLine用于定义媒体的结构,还提供了一个MediaPeriod用于表示TimeLine的时间段
- Render: 传统上Android的Render只是指把图片或文本显示到显示器的过程,而这里的Render与Android的概念有点差异,虽然同样把帧数据呈现给用户,但这里不仅指视频和文字帧,也指音频帧
- Decode: 解码, 如音频解码,视频解码,文字解码等, 一般表示把数据从压缩还原成可以渲染的原始数据的过程
从样例代码中,我们看到了二个Factory类: MediaSourceFactory及RenderersFactory 从设计模式中我们知道,Factory类是工厂类,用于负责实际创建需要使用的产品,如RendersFactory肯定就是用于创造Render 如下是Exoplayer的Glossary里提供的总体架构图,这张图是我们理解ExoPlayer的核心,Exoplayer在使用时,并没有一个非常简单的Facade类,而是需要用户自己装配其中各个组件; 而这个装配过程,就是ExoPlayer优雅架构及可扩展的核心。
如下Render相关的架构图, 从这张图里可以看到,与我们通常理解的先decode后render的过程不一样, ExoPlayer是通过Render把Decoder包装起来的,二者并不是独立的。
3 工程组织
Exoplayer工程里最重要的三个目录:
- demos: 项目演示工具,非对外发布的库,只是用于告诉大家如何使用
- extensions: 扩展库,这里的库都是依赖于其他的一些开源项目才能运行,编译步骤也会复杂一些。 但vp9和av1的支持,就需要依赖于开源的c/c++的专门的编解码开源软件
- library: ExoPlayer库的核心功能都在这里,但它对外发布时,有一些特殊的能力,如dsh, hls, rtsp并不属于核心库的一部分,早分开来作为可扩展的能力发布的
通过readme-graph生成整个工程的模块依赖关系图如下
工程组织关系由上至下主要分为以下几个层级:
- app层
- extension层:各种扩展,如ffmpeg, vp9, av1这几个依赖于decoder; 也依赖core; 而cronet和okhttp的扩展则依赖datasource;扩展层是在library-core以上的,因此应用可以按需集成
- library-core: 它定义了播放器所需要的各个组件的协调关系
- 播放组件层:datasource, decoder, extractor 这一层主要定义了library-core依赖的重要的接口,如果我们学习过SOLID中的依赖倒置原则,应该就更好理解为什么是接口
- library-common层:这一层主要就是定义了用到和各种基本的数据类型或util类,如MediaItem, Metadata, Player等
4 vp9扩展解码的实现
4.1 编译构建extension-vp9
vp9是google推出来的新的视频编解码技术,可以免费使用,开源友好,同等视频质量下,视频压缩率比h264要高很多,现在很多开发板厂商都集成硬解vp9的库,我在家里看油管,一般都优先选择vp9的视频格式。 我们以vp9的扩展使用和定义过程,更进一步理解ExoPlayer的extension扩展能力。 根据extensions/vp9路径下的README.md文件,我们知道vp9的编译依赖于google开源的libvpx库,并需要下载ndk进行编译, 但只支持在linux环境和mac Os系统下编译。 具体的编译步骤,可以参考:ExoPlayer vp9目录中的README.md
4.2 使用vp9扩展
在使用时,可以自己定制或实现RenderFactory的子实例,需要在返回的列表中,有一个LibvpxVideoRenderer。 当然也可以直接使用DefaultRendersFactory,并设置extensionRenderMode的参数:
new DefaultRenderersFactory(context.getApplicationContext()).setExtensionRendererMode(extensionRendererMode);
参数取值范围如下:
- EXTENSION_RENDERER_MODE_OFF: 关闭扩展
- EXTENSION_RENDERER_MODE_ON: 打开扩展,优先使用核心库
- EXTENSION_RENDERER_MODE_PREFER: 打开扩展,优先使用扩展库
4.3 vp9相关的类图
可以看到,extension-vp9模块最重要的就是定义了LibvpxVideoRenderer及VpxDecoder, 而这二个类对应的父类,分别定义于library-core及library-decoder模块。
4.4 DefaultRenderersFactory源码分析
上面的DefaultRenderersFactory里,有一个不对劲的地方。 我们前面说了core模块是底层的模块,它是看不到LibvpxVideoRenderer类的,它怎么能使用它呢? 打开DefaultRenderersFactory.buildVideoRenderers函数:
// 前面的代码添加了缺省的Render到out数组里, 限于篇幅,就不列出代码
// 可以看到prefer的作用就在于,到底添加新元素时,是添加在原数据的前面,还是添加在原数据的后面
int extensionRendererIndex = out.size();
if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
extensionRendererIndex--;
}
try {
// Full class names used for constructor args so the LINT rule triggers if any of them move.
// 答案揭晓,反射大法永远是那么有效。考虑到这个只在播放前创建时播放器时使用,因此这个反射不会影响播放效率
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
Constructor<?> constructor =
clazz.getConstructor(
long.class,
android.os.Handler.class,
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
int.class);
Renderer renderer =
(Renderer)
constructor.newInstance(
allowedVideoJoiningTimeMs,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibvpxVideoRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
// The extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating VP9 extension", e);
}
// 后面还有添加其他extentsion类的,也不再列出
5 参考文档
|