ijkplayer 学习笔记

ijk概述
mediacodec相关
OpenGL相关
filter相关
setOption配置相关
metadata相关
h264编码器特有的设置域
线程相关
消息机制
音频输出
声道切换
SDL_CreateCond 与 SDL_CreateThreadEx
如何暂停

笔记可能微乱,但大致清晰,可能会对他人有所帮助,故分享出来。


ijk概述


《零基础读懂视频播放器控制原理: ffplay 播放器源代码分析》: https://cloud.tencent.com/developer/article/1004559
《基于 ffmpeg 的跨平台播放器实现》: https://cloud.tencent.com/developer/article/1004561
《ijkplayer框架深入剖析》: https://www.jianshu.com/p/daf0a61cc1e0

三种播放器实现: 均继承自 AbstractMediaPlayer 继承自 IMediaPlayer
1.AndroidMediaPlayer:基于安卓自带播放器(位于ijkplayer-java)
2.IjkExoMediaPlayer:基于ExoPlayer(位于ijkplayer-exo)
介绍:http://blog.csdn.net/u014606081/article/details/76181049
3.IjkMediaPlayer:基于ffplay(位于ijkplayer-java,底层实现在ijkmedia目录)
输出:
video-output: NativeWindow, OpenGL ES 2.0
audio-output: AudioTrack, OpenSL ES
jni底层接口:
IjkMediaPlayer : ijkmedia/ijkplayer/android/ijkplayer_jni.c
播放器结构体:VideoState(ff_ffplay_def.c )
播放入口:
ffplay.c : ffp_prepare_async_l
stream_open :创建音视频解码前后队列, 创建数据读取(read_thread)和视频显示线程(video_refresh_thread)
1.read_thread:读取packet
stream_component_open: 打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程
av_read_frame:读取媒体数据,得到的是音视频分离的解码前数据
packet_queue_put:往缓冲队列中放入解码前的音、视、字幕 packet
打开视频解码器:
ffplay.c : stream_component_open
ffpipeline_open_video_decoder
ff_ffpipeline.c : ffpipeline_open_video_decoder 调用 IJKFF_Pipeline->func_open_video_decoder
IJKFF_Pipeline->func_open_video_decoder 函数指针指向 android/pipeline/cffpipeline_android.c 的 func_open_video_decoder 方法
然后调用 android/pipeline/ffpipenode_android_mediacodec_vdec.c 的 ffpipenode_create_video_decoder_from_android_mediacodec
2.video_thread:解码
ff_ffpipenode.c : ffpipenode_run_sync
/android/pipeline/ffpipenode_android_mediacodec_vdec.c : func_run_sync 解码后的帧加入帧缓冲队列
IJKFF_Pipenode IJKFF_Pipenode_Opaque
视频显示:
video_refresh_thread
video_refresh
frame_queue_peek_last
video_display2
video_image_display2
ijksdl/ijksdl_vout.c : SDL_VoutDisplayYUVOverlay
ijksdl_vout_android_nativewindow.c vout->display_overlay = func_display_overlay
func_display_overlay
func_display_overlay_l
音视频同步:
video_refresh


mediacodec相关


1.为什么ijk硬解不用ffmpeg自带的mediacodec_wrapper,而是自己在底层封装的java api
https://github.com/Bilibili/ijkplayer/issues/1705
https://github.com/Bilibili/ijkplayer/issues/1557
FFmpeg3.1中也集成了MediaCodec硬件解码
ijkplayer doesn’t use ffmpeg’s mediacodec implement
ijkplayer has its own mediacodec implement.


OpenGL相关


1.Android MediaCodec and OpenGL Render
https://github.com/Bilibili/ijkplayer/issues/2893
2.ijk android是支持opengl的
https://github.com/Bilibili/ijkplayer/issues/338

ijksdl/gles2/fsh/ 下脚本可修改gl渲染效果

问题:
播放时软解解码出的为yuv420p格式,可通过gl渲染
播放时硬解解码出不能通过gl渲染,视频帧格式为 QCOM_FORMATYUV420PackedSemiPlanar32m
分析:
不同设备硬解出的视频帧格式不同:
https://www.polarxiong.com/archives/Android-MediaCodec%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6%E7%A1%AC%E4%BB%B6%E8%A7%A3%E7%A0%81-%E9%AB%98%E6%95%88%E7%8E%87%E5%BE%97%E5%88%B0YUV%E6%A0%BC%E5%BC%8F%E5%B8%A7-%E5%BF%AB%E9%80%9F%E4%BF%9D%E5%AD%98JPEG%E5%9B%BE%E7%89%87-%E4%B8%8D%E4%BD%BF%E7%94%A8OpenGL.html
比如红米设备上硬解出的帧格式为:YUV420PackedSemiPlanar32m
ijk中的gl渲染只支持yuv420、rgb等,需要把 YUV420PackedSemiPlanar32m 转为 YUV 才可以
转换算法:http://blog.csdn.net/u011270282/article/details/50698243
解决:
播放时的解码速度并不严格要求,先用软解,软解解出的为yuv420p,可以直接gl渲染
遗留优化项:
若采用软解播放则进行内存比较,若硬解较优,则硬解后将视频帧转换为gl可以渲染的格式,当然也要考虑转换的时间消耗


filter相关


1.Does ijkplayer support video filters , ijk支持java层直接设置滤镜!
https://github.com/Bilibili/ijkplayer/issues/3686


setOption配置相关


mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, “mediacodec”, 0);
“mediacodec”等配置项位于:ff_ffplayer_options.h 以及 option_table.h
如何利用 AVDictionary 配置参数 : http://blog.csdn.net/encoder1234/article/details/54582676

AVClass 与 AVOption : http://blog.csdn.net/leixiaohua1020/article/details/44268323
AVClass就是AVOption和目标结构体之间的“桥梁”。
AVClass中存储了AVOption类型的数组option,用于存储选项信息。
AVClass有一个特点就是它必须位于其支持的结构体的第一个位置


metadata相关


对于metadata的操作封装于 ijkmeta.c , metadata信息声明在ijkmeta.h中, java层对应IjkMediaMeta.java
IjkMediaMeta分两层,外层metadata,内层:video、audio、subtitle三个metadata,共4个metadata
显然 ijkmeta.c 中只针对播放流程,也就是只负责从视频中获取 metadata ,然后在播放时获取 metadata 信息
保存时应该将metadata信息写入视频,首先要搞清楚需要写入哪些metadata信息?
应该是:凡是播放时需要获取的都需要写入,这样才能正常播放
How to set header metadata to encoded video?
https://stackoverflow.com/questions/17024192/how-to-set-header-metadata-to-encoded-video
avformat.h有对metadata的用法介绍


h264编码器特有的设置域


具体见:libx264.c options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* priv_data 属于每个编码器特有的设置域,用 av_opt_set 设置  */
/**
* preset : 编码模式
* ultrafast,superfast, veryfast, faster, fast, medium, slow, slower, veryslow
* fast 节省约 10% encoding time 10s视频100s
* faster 25%
* ultrafast 55% 10s视频16s
* 但越快质量越低
*/

av_opt_set(enc_ctx->priv_data, "preset", "ultrafast", 0);

/**
* lookahead:编码码率控制所需要锁定的帧个数
*/
av_opt_set(enc_ctx->priv_data, "lookahead", "0", 0);

/**
* 使用2pass编码模式
* 1pass和2pass的区别在于1pass只需要编码一次,
* 2pass需要编码两次。2pass的优点在于可编码更小的文件,缺点在于所花费时间比1pass更多
*/
av_opt_set(enc_ctx->priv_data, "2pass", "0", 0);

/**
* 无延时输出
*/
av_opt_set(enc_ctx->priv_data, "zerolatency", "1", 0);


线程相关


ijk线程相关操作位于ijksdl_thread.c,封装了pthread库
以解码为例:
d->decoder_tid = SDL_CreateThreadEx(&d->_decoder_tid, fn, arg, name);
在 SDL_CreateThreadEx 方法其实是调用 pthread_create 方法
ijk 封装了线程的初始化和销毁操作,见 ijksdl_thread.c 的 SDL_RunThread方法


消息机制


native_setup 时设置循环读消息函数, mp->msg_loop = msg_loop ,初始化消息队列在 FFPlayer 中
在 ijkmp_prepare_async_l 时创建线程运行循环读消息函数


音频输出


新建播放器实例(native_setup)时,根据ffp->opensles 初始化 opensles 或 androidTrack 输出设备
在 opensles 或 androidTrack 中,通过回调 sdl_audio_callback 方法从音频帧缓冲队列中读取音频数据,播放之
比如在 ijksdl_aout_android_opensles.c 中 audio_cblk 就是 sdl_audio_callback 方法


声道切换


ijkplayer如何切换音轨,以及获取音轨信息:https://github.com/Bilibili/ijkplayer/issues/3811
IjkMediaPlayer.java 跟轨道相关的方法 :
getTrackInfo(获取所有轨道)、getSelectedTrack(获取当前轨道)、selectTrack(选择轨道)
selectTrack(选择轨道)可实现切换轨道操作
对应调用 ff_ffplayer.c 的 ffp_set_stream_selected 方法
此方法中通过调用 stream_component_close、stream_component_open 实现轨道切换


SDL_CreateCond 与 SDL_CreateThreadEx


ff_ffplay.c 中有一句代码: is->continue_read_thread = SDL_CreateCond()
乍一看变量名会以为 SDL_CreateCond 方法是用来创建线程… 不对! ,SDL_CreateThreadEx 才是用来创建线程的
SDL_CreateCond 方法调用了 pthread_cond_init 方法,含义为:创建条件变量
条件变量相关:
1.初始化条件变量 pthread_cond_init
2.阻塞在条件变量上pthread_cond_wait
3.解除在条件变量上的阻塞pthread_cond_signal
4.阻塞直到指定时间pthread_cond_timedwait
5.释放阻塞的所有线程pthread_cond_broadcast
6.释放条件变量pthread_cond_destroy
详细:https://blog.csdn.net/ithomer/article/details/6031723
ijksdl_mutex.c 中封装了互斥锁相关操作


如何暂停


IjkMediaPlayer.java 中的 _pause 本地方法调用 ijkplayer_jni.c 中的 IjkMediaPlayer_pause
然后通过ijk的消息机制发送暂停信号: ffp_notify_msg1(mp->ffplayer, FFP_REQ_PAUSE);
然后调用到 ff_ffplay.c 的 ffp_pause_l 方法,最后调用到 stream_toggle_pause_l 方法
其中两个关键操作:1.is->paused 置 true ; 2.调用 SDL_AoutPauseAudio 方法
其实在读包、解码、渲染/播放这个工作线中,只要有一处停止工作,其他地方自然会阻塞住即可,
但是看代码发现多处都有根据is->paused来停止工作的逻辑,列一下自认为比较关键的地方:
对于视频:在视频渲染线程的 video_refresh 方法中:
if (is->paused) goto display;
如果暂停了,就一直显示当前帧,跳过后面的取下一帧操作,不取下一帧,帧队列满后自然就会停止解码、读包操作。
对于音频: 调用 SDL_AoutPauseAudio 方法后不再回调 sdl_audio_callback 方法(参见上面的《音频输出》)