IjkPlayer 学习笔记

作者: 王英豪 | 来源:发表于2019-03-01 10:52 被阅读20次

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

    ×××××××××××××××××××目录×××××××××××××××××××
    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

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

    相关文章

      网友评论

        本文标题:IjkPlayer 学习笔记

        本文链接:https://www.haomeiwen.com/subject/wtgruqtx.html