Ahab's Studio.

ijkplayer 学习笔记

字数统计: 2k阅读时长: 8 min
2018/05/01 Share

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

目录:

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 方法(参见上面的《音频输出》)

原文作者:Ahab

原文链接:http://yhaowa.gitee.io/306f0479/

发表日期:May 1st 2018, 10:53:54 am

更新日期:May 23rd 2020, 10:57:30 am

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. ijk概述
  2. 2. mediacodec相关
  3. 3. OpenGL相关
  4. 4. filter相关
  5. 5. setOption配置相关
  6. 6. metadata相关
  7. 7. h264编码器特有的设置域
  8. 8. 线程相关
  9. 9. 消息机制
  10. 10. 音频输出
  11. 11. 声道切换
  12. 12. SDL_CreateCond 与 SDL_CreateThreadEx
  13. 13. 如何暂停