ijkplayer框架详解

  1. 云栖社区>
  2. 博客>
  3. 正文

ijkplayer框架详解

code_xzh 2017-11-25 11:44:59 浏览4282
展开阅读全文

不少读者很多是以ijkplayer为播放内核。因为编译简单,接口使用方便。核心部分是移植了ffplay.c中的代码。今天分享这篇文章是金山云团队,调研分析的ijkplayer框架代码。涉及了一些基本流程。对于想了解ijkplayer的同学算是很好的借鉴。

随着互联网技术的飞速发展,移动端播放视频的需求如日中天,由此也催生了一批开源/闭源的播放器,但是无论这个播放器功能是否强大、兼容性是否优秀,它的基本模块通常都是由以下部分组成:事务处理、数据的接收和解复用、音视频解码以及渲染,其基本框架如下图所示:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

播放器基本框图

针对各种铺天盖地的播放器项目,我们选取了比较出众的ijkplayer进行源码剖析。它是一个基于FFPlay的轻量级Android/iOS视频播放器,实现了跨平台的功能,API易于集成;编译配置可裁剪,方便控制安装包大小。

本文基于k0.7.6版本的ijkplayer,重点分析其C语言实现的核心代码,涉及到不同平台下的封装接口或处理方式时,均以iOS平台为例,Android平台大同小异,请大家自行查阅研究。

一、总体说明

打开ijkplayer,可看到其主要目录结构如下:

  • tool 初始化项目工程脚本

  • config 编译ffmpeg使用的配置文件

  • extra 存放编译ijkplayer所需的依赖源文件, 如ffmpeg、openssl等

  • ijkmedia 核心代码 

    • ijkplayer 播放器数据下载及解码相关

    • ijksdl 音视频数据渲染相关

  • ios iOS平台上的上层接口封装以及平台相关方法

  • android android平台上的上层接口封装以及平台相关方法


在功能的具体实现上,iOS和Android平台的差异主要表现在视频硬件解码以及音视频渲染方面,两者实现的载体区别如下表所示:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA

二、初始化流程

初始化完成的主要工作就是创建播放器对象,打开ijkplayer/ios/IJKMediaDemo/IJKMediaDemo.xcodeproj工程,可看到IJKMoviePlayerViewController类中viewDidLoad方法中创建了IJKFFMoviePlayerController对象,即iOS平台上的播放器对象。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA查看ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer/IJKFFMoviePlayerController.m文件,其初始化方法具体实现如下:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA可发现在此创建了IjkMediaPlayer结构体实例_mediaPlayer:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA在该方法中主要完成了三个动作:创建IJKMediaPlayer对象gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA通过ffp_create方法创建了FFPlayer对象,并设置消息处理函数。

创建图像渲染对象SDL_Voutgif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA至此已经完成了ijkplayer播放器初始化的相关流程,简单来说,就是创建播放器对象,完成音视频解码、渲染的准备工作。在下一章节中,会重点介绍播放的核心代码。

三、核心代码剖析

ijkplayer实际上是基于ffplay.c实现的,本章节将以该文件为主线,从数据接收、音视频解码、音视频渲染及同步这三大方面进行讲解,要求读者有基本的ffmpeg知识。

ffplay.c中主要的代码调用流程如下图所示:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA

当外部调用prepareToPlay启动播放后,ijkplayer内部最终会调用到ffplay.c中的

int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)

方法,该方法是启动播放器的入口函数,在此会设置player选项,打开audio output,最重要的是调用stream_open方法。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA从代码中可以看出,stream_open主要做了以下几件事情:

  • 创建存放video/audio解码前数据的videoq/audioq

  • 创建存放video/audio解码后数据的pictq/sampq

  • 创建读数据线程read_thread

  • 创建视频渲染线程video_refresh_thread

说明:subtitle是与video、audio平行的一个stream,ffplay中也支持对它的处理,即创建存放解码前后数据的两个queue,并且当文件中存在subtitle时,还会启动subtitle的解码线程,由于篇幅有限,本文暂时忽略对它的相关介绍。

3.1 数据读取

数据读取的整个过程都是由ffmpeg内部完成的,接收到网络过来的数据后,ffmpeg根据其封装格式,完成了解复用的动作,我们得到的,是音视频分离开的解码前的数据,步骤如下:

创建上下文结构体,这个结构体是最上层的结构体,表示输入上下文gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA设置中断函数,如果出错或者退出,就可以立刻退出gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA读取媒体数据,得到的是音视频分离的解码前数据gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA将音视频数据分别送入相应的queue中gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA重复6、7步,即可不断获取待播放的数据。

3.2 音视频解码

ijkplayer在视频解码上支持软解和硬解两种方式,可在起播前配置优先使用的解码方式,播放过程中不可切换。iOS平台上硬解使用VideoToolbox,Android平台上使用MediaCodec。ijkplayer中的音频解码只支持软解,暂不支持硬解。

3.2.1 视频解码方式选择

在打开解码器的方法中:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=首先会打开ffmpeg的解码器,然后通过ffpipeline_open_video_decoder创建IJKFF_Pipenode。

第二章节中有介绍,在创建IJKMediaPlayer对象时,通过ffpipeline_create_from_ios创建了pipeline,则gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAfunc_open_video_decoder函数指针最后指向的是ffpipeline_ios.c中的func_open_video_decoder,其定义如下:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA如果配置了ffp->videotoolbox,会优先去尝试打开硬件解码器,

node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);

如果硬件解码器打开失败,则会自动切换至软解

node = ffpipenode_create_video_decoder_from_ffplay(ffp);

ffp->videotoolbox需要在起播前通过如下方法配置:

ijkmp_set_option_int(_mediaPlayer, IJKMP_OPT_CATEGORY_PLAYER,

"videotoolbox", 1);


3.2.2 音视频解码

video的解码线程为video_thread,audio的解码线程为audio_thread

不管视频解码还是音频解码,其基本流程都是从解码前的数据缓冲区中取出一帧数据进行解码,完成后放入相应的解码后的数据缓冲区,如下图所示:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA

音视频解码示意图

本文以video的软解流程为例进行分析,audio的流程可对照研究。

视频解码线程gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAffpipenode_run_sync中调用的是IJKFF_Pipenode对象中的func_run_syncgif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAfunc_run_sync取决于播放前配置的软硬解,假设为软解,则调用gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAget_video_frame中调用了decoder_decode_frame,其定义如下:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA该方法中从解码前的video queue中取出一帧数据,送入decoder进行解码,解码后的数据在ffplay_video_thread中送入pictq。

3.3 音视频渲染及同步

3.3.1 音频输出

ijkplayer中Android平台使用OpenSL ES或AudioTrack输出音频,iOS平台使用AudioQueue输出音频。

audio output节点,在ffp_prepare_async_l方法中被创建:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAffpipeline_open_audio_output方法实际上调用的是IJKFF_Pipeline对象的函数指针func_open_audio_utput,该函数指针在初始化中的ijkmp_ios_create方法中被赋值,最后指向的是func_open_audio_outputgif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA回到ffplay.c中,如果发现待播放的文件中含有音频,那么在调用stream_component_open打开解码器时,该方法里面也调用audio_open打开了audio output设备。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAaudio_open中配置了音频输出的相关参数SDL_AudioSpec,并通过gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA设置给了Audio Output, iOS平台上即为AudioQueue。

AudioQueue模块在工作过程中,通过不断的callback来获取pcm数据进行播放。

有关AudioQueue的具体内容此处不再介绍。

3.3.2 视频渲染

iOS平台上采用OpenGL渲染解码后的YUV图像,渲染线程为video_refresh_thread,最后渲染图像的方法为video_image_display2,定义如下:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA从代码实现上可以看出,该线程的主要工作为:

调用frame_queue_peek_last从pictq中读取当前需要显示视频帧

调用SDL_VoutDisplayYUVOverlay进行绘制gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAdisplay_overlay函数指针在前面初始化流程有介绍过,它在gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA方法中被赋值为vout_display_overlay,该方法就是调用OpengGL绘制图像。

3.4.3 音视频同步

对于播放器来说,音视频同步是一个关键点,同时也是一个难点,同步效果的好坏,直接决定着播放器的质量。通常音视频同步的解决方案就是选择一个参考时钟,播放时读取音视频帧上的时间戳,同时参考当前时钟参考时钟上的时间来安排播放。如下图所示:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA

音视频同步示意图

如果音视频帧的播放时间大于当前参考时钟上的时间,则不急于播放该帧,直到参考时钟达到该帧的时间戳;如果音视频帧的时间戳小于当前参考时钟上的时间,则需要“尽快”播放该帧或丢弃,以便播放进度追上参考时钟。

参考时钟的选择也有多种方式:

  • 选取视频时间戳作为参考时钟源

  • 选取音频时间戳作为参考时钟源

  • 选取外部时间作为参考时钟源


考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。

ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染video_refresh_thread的线程中:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA从上述实现可以看出,该方法中主要循环做两件事情:

  1. 休眠等待,remaining_time的计算在video_refresh

  2. 调用video_refresh方法,刷新视频帧

可见同步的重点是在video_refresh中,下面着重分析该方法:gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAlastvp是上一帧,vp是当前帧,last_duration则是根据当前帧和上一帧的pts,计算出来上一帧的显示时间,经过compute_target_delay方法,计算出显示当前帧需要等待的时间。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAcompute_target_delay方法中,如果发现当前主时钟源不是video,则计算当前视频时钟与主时钟的差值:

  • 如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;

  • 如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间

  • 如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。

回到video_refreshgif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAframe_timer实际上就是上一帧的播放时间,而frame_timer + delay实际上就是当前这一帧的播放时间,如果系统时间还没有到当前这一帧的播放时间,直接跳转至display,而此时is->force_refresh变量为0,不显示当前帧,进入video_refresh_thread中下一次循环,并睡眠等待。gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAA如果当前这一帧的播放时间已经过了,并且其和当前系统时间的差值超过了AV_SYNC_THRESHOLD_MAX,则将当前这一帧的播放时间改为系统时间,并在后续判断是否需要丢帧,其目的是为后面帧的播放时间重新调整frame_timer,如果缓冲区中有更多的数据,并且当前的时间已经大于当前帧的持续显示时间,则丢弃当前帧,尝试显示下一帧。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=否则进入正常显示当前帧的流程,调用video_display2开始渲染。

网友评论

登录后评论
0/500
评论
code_xzh
+ 关注