ijkplayer 是一款比较出众的开源 Android/IOS 跨平台播放器,基于 ffplay,API 易于集成,可定制编译控制体积。
本文基于 0.8.8 版本的 ijkplayer ,对其源码进行剖析,涉及到不同平台下的封装接口或处理方式时,均以 Android 为例。
ijkplayer android 集成了三种播放器实现:
- AndroidMediaPlayer:即安卓系统自带的播放器 MediaPlayer,基于 MediaCodec、AudioTrack 等安卓系统 API.
- IjkExoMediaPlayer:即谷歌新推出的 ExoPlayer,同样是基于 MediaCodec、AudioTrack 等安卓系统 API,但相比 MediaPlayer 具有支持 DASH、高级 HLS、自定义扩展等优点。
- IjkMediaPlayer:基于 FFmpeg 的 ffplay,集成了 MediaCodec 硬解码器、Opengl 渲染方式等。
一般而言, ijkplayer 就是指 IjkMediaPlayer,本文分析的对象就是 IjkMediaPlayer.
目录结构
123456789 | ijkplayer(项目文件夹) ├──tools - 初始化项目工程脚本 ├──config - 编译ffmpeg使用的配置文件 ├──extra - 存放编译ijkplayer所需的依赖源文件, 如ffmpeg、openssl等 ├──ijkmedia - 核心代码 ├──ijkplayer - 播放器数据下载及解码相关 ├──ijksdl - 音视频数据渲染相关 ├──android - android平台上的上层接口封装以及平台相关方法 ├──ios - iOS平台上的上层接口封装以及平台相关方法 |
功能实现的平台差异
iOS和Android平台的差异主要表现在
- 视频硬件解码
- 音频渲染
- 视频渲染
Platform | Hardware Codec | Video Render | Audio Output |
---|---|---|---|
Android | MediaCodec | OpenGL ES、MediaCodec | OpenSL ES、AudioTrack |
iOS | VideoToolBox | OpenGL ES | AudioQueue |
IjkMediaPlayer和Native交互
播放控制相关的 start、pause、stop
等,调用 对应的 native 方法
底层状态信息的上报(比如底层的播放状态回调)相关的 postEventFromNative
等,这些方法由底层主动调用(有@CalledByNative
注解)
初始化
123456789101112131415161718192021 | public final class extends AbstractMediaPlayer { private void initPlayer(IjkLibLoader libLoader) { loadLibrariesOnce(libLoader); initNativeOnce(); Looper looper; if ((looper = Looper.myLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else if ((looper = Looper.getMainLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else { mEventHandler = null; } * Native setup requires a weak reference to our object. It's easier to * create it here than in C++. */ native_setup(new WeakReference<IjkMediaPlayer>(this)); }} |
initPlayer
方法,共做了四件事:
- 加载 so 库
- 静态初始化底层,底层其实什么都没做
- 初始化 Message Handler,处理底层状态信息的上报
- 初始化底层,这部分做的工作最多
初始化底层
c代码
ijkplayer/android/ijkplayer/ijkplayer-armv7a/src/main/jni/ijkmedia/ijkplayer/android/
主要逻辑位于
ijkpalyer_android.c
的 ijkmp_android_create
方法
12345678 | // 创建底层播放器对象,设置消息处理函数IjkMediaPlayer *mp = ijkmp_create(msg_loop, saveMode, hard_mux);// 创建图像渲染对象mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();// 初始化视频解码器(软/硬)、音频输出设备(opensles/audioTrack)mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer); |
软硬解码选择
跟踪 pipeline/ffpipeline_android.c
的 ffpipeline_create_from_android
方法
发现是 用函数指针记录 类 IjkMediaPlayer.setOption() 设置的属性
配置
初始化后 IjkMediaPlayer 后,可以对其进行一系列配置,例如:
12345 | // 设置硬解码mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);// 设置 opensles 方式输出音频mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1); |
setOption() 会调用到底层 ff_ffplay.c 的 ffp_set_option() 方法
播放
播放器必然是通过多线程同时进行解封装、解码、视频渲染等工作的,对于 Ijkplayer 来说,开辟的线程如下:
当对播放器设置视频源路径、解码方式、输出模式等播放选项后,就可以开始播放了, 播放入口方法为 ffp_prepare_async_l
,此方法中调用了比较重要的两个方法:
12345 | // 打开音频输出设备 ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp); ... // 创建音/视频数据解码前/后队列, 创建解封装和视频渲染线程 VideoState *is = stream_open(ffp, file_name, NULL); |
stream_open 方法则相当重要了,梳理一下该方法中涉及到的关键方法:
音视频同步
对于播放器来说,音视频同步是一个关键点,同时也是一个难点,同步效果的好坏,直接决定着播放器的质量。
通常音视频同步的解决方案就是选择一个参考时钟,播放时读取音视频帧上的时间戳,同时参考当前时钟参考时钟上的时间来安排播放。如下图所示:
如果音视频帧的播放时间大于当前参考时钟上的时间,则不急于播放该帧,直到参考时钟达到该帧的时间戳;如果音视频帧的时间戳小于当前参考时钟上的时间,则需要“尽快”播放该帧或丢弃,以便播放进度追上参考时钟。
参考时钟的选择
有多种方式:
- 选取视频时间戳作为参考时钟源
- 选取音频时间戳作为参考时钟源
- 选取外部时间作为参考时钟源
考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。
ijkplayer在默认情况下也是使用音频作为参考时钟源
事件处理
在播放过程中,某些行为的完成或者变化,如prepare完成,开始渲染等,需要以事件形式通知到外部,以便上层作出具体的业务处理。
播放器底层上报事件时,实际上就是将待发送的消息放入消息队列,另外有一个线程会不断从队列中取出消息,上报给外部
参考&扩展
原文:大专栏 ijkplayer剖析