固阳音箱协会

ijkplayer框架深入剖析

何俊林2019-01-17 07:29:48

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

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

播放器基本框图

针对各种铺天盖地的播放器项目,我们选取了比较出众的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平台的差异主要表现在视频硬件解码以及音视频渲染方面,两者实现的载体区别如下表所示:

二、初始化流程

初始化完成的主要工作就是创建播放器对象,打开ijkplayer/ios/IJKMediaDemo/IJKMediaDemo.xcodeproj工程,可看到IJKMoviePlayerViewController类中viewDidLoad方法中创建了IJKFFMoviePlayerController对象,即iOS平台上的播放器对象。ffp_create方法创建了FFPlayer对象,并设置消息处理函数。

创建图像渲染对象SDL_Vout

三、核心代码剖析

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

ffplay.c中主要的代码调用流程如下图所示:

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

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

方法,该方法是启动播放器的入口函数,在此会设置player选项,打开audio output,最重要的是调用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根据其封装格式,完成了解复用的动作,我们得到的,是音视频分离开的解码前的数据,步骤如下:

创建上下文结构体,这个结构体是最上层的结构体,表示输入上下文

3.2 音视频解码

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

3.2.1 视频解码方式选择

在打开解码器的方法中:ffpipeline_open_video_decoder创建IJKFF_Pipenode。

第二章节中有介绍,在创建IJKMediaPlayer对象时,通过ffpipeline_create_from_ios创建了pipeline,则func_open_video_decoder函数指针最后指向的是ffpipeline_ios.c中的func_open_video_decoder,其定义如下:

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

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

音视频解码示意图

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

视频解码线程ffpipenode_run_sync中调用的是IJKFF_Pipenode对象中的func_run_syncfunc_run_sync取决于播放前配置的软硬解,假设为软解,则调用get_video_frame中调用了decoder_decode_frame,其定义如下:ffplay_video_thread中送入pictq。

3.3 音视频渲染及同步

3.3.1 音频输出

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

audio output节点,在ffp_prepare_async_l方法中被创建:ffpipeline_open_audio_output方法实际上调用的是IJKFF_Pipeline对象的函数指针func_open_audio_utput,该函数指针在初始化中的ijkmp_ios_create方法中被赋值,最后指向的是func_open_audio_outputstream_component_open打开解码器时,该方法里面也调用audio_open打开了audio output设备。audio_open中配置了音频输出的相关参数SDL_AudioSpec,并通过

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

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

3.3.2 视频渲染

iOS平台上采用OpenGL渲染解码后的YUV图像,渲染线程为video_refresh_thread,最后渲染图像的方法为video_image_display2,定义如下:

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

调用SDL_VoutDisplayYUVOverlay进行绘制vout_display_overlay,该方法就是调用OpengGL绘制图像。

3.4.3 音视频同步

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

音视频同步示意图

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

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

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

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

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


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

ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染video_refresh_thread的线程中:

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

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

可见同步的重点是在video_refresh中,下面着重分析该方法:compute_target_delay方法,计算出显示当前帧需要等待的时间。compute_target_delay方法中,如果发现当前主时钟源不是video,则计算当前视频时钟与主时钟的差值:

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

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

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

回到video_refreshframe_timer实际上就是上一帧的播放时间,而frame_timer + delay实际上就是当前这一帧的播放时间,如果系统时间还没有到当前这一帧的播放时间,直接跳转至display,而此时is->force_refresh变量为0,不显示当前帧,进入video_refresh_thread中下一次循环,并睡眠等待。AV_SYNC_THRESHOLD_MAX,则将当前这一帧的播放时间改为系统时间,并在后续判断是否需要丢帧,其目的是为后面帧的播放时间重新调整frame_timer,如果缓冲区中有更多的数据,并且当前的时间已经大于当前帧的持续显示时间,则丢弃当前帧,尝试显示下一帧。video_display2开始渲染。

Copyright © 固阳音箱协会@2017