ijkplayer框架深入剖析

不少读者很多是以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平台上的播放器对象。

查看ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer/IJKFFMoviePlayerController.m文件,其初始化方法具体实现如下:

可发现在此创建了IjkMediaPlayer结构体实例_mediaPlayer:

在该方法中主要完成了三个动作:创建IJKMediaPlayer对象

通过ffp_create方法创建了FFPlayer对象,并设置消息处理函数。

创建图像渲染对象SDL_Vout

创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分

至此已经完成了ijkplayer播放器初始化的相关流程,简单来说,就是创建播放器对象,完成音视频解码、渲染的准备工作。在下一章节中,会重点介绍播放的核心代码。

三、核心代码剖析

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方法。

从代码中可以看出,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根据其封装格式,完成了解复用的动作,我们得到的,是音视频分离开的解码前的数据,步骤如下:

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

设置中断函数,如果出错或者退出,就可以立刻退出

打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等

探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息

打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。

读取媒体数据,得到的是音视频分离的解码前数据

将音视频数据分别送入相应的queue中

重复6、7步,即可不断获取待播放的数据。

3.2 音视频解码

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

3.2.1 视频解码方式选择

在打开解码器的方法中:

首先会打开ffmpeg的解码器,然后通过ffpipeline_open_video_decoder创建IJKFF_Pipenode。

第二章节中有介绍,在创建IJKMediaPlayer对象时,通过ffpipeline_create_from_ios创建了pipeline,则

func_open_video_decoder函数指针最后指向的是ffpipeline_ios.c中的func_open_video_decoder,其定义如下:

如果配置了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

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

音视频解码示意图

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

视频解码线程

ffpipenode_run_sync中调用的是IJKFF_Pipenode对象中的func_run_sync

func_run_sync取决于播放前配置的软硬解,假设为软解,则调用

get_video_frame中调用了decoder_decode_frame,其定义如下:

该方法中从解码前的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方法中被创建:

ffpipeline_open_audio_output方法实际上调用的是IJKFF_Pipeline对象的函数指针func_open_audio_utput,该函数指针在初始化中的ijkmp_ios_create方法中被赋值,最后指向的是func_open_audio_output

回到ffplay.c中,如果发现待播放的文件中含有音频,那么在调用stream_component_open打开解码器时,该方法里面也调用audio_open打开了audio output设备。

audio_open中配置了音频输出的相关参数SDL_AudioSpec,并通过

设置给了Audio Output, iOS平台上即为AudioQueue。

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

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

3.3.2 视频渲染

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

从代码实现上可以看出,该线程的主要工作为:

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

调用SDL_VoutDisplayYUVOverlay进行绘制

display_overlay函数指针在前面初始化流程有介绍过,它在

方法中被赋值为vout_display_overlay,该方法就是调用OpengGL绘制图像。

3.4.3 音视频同步

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

音视频同步示意图

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

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

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

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

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

从上述实现可以看出,该方法中主要循环做两件事情:

  1. 休眠等待,remaining_time的计算在video_refresh
  2. 调用video_refresh方法,刷新视频帧

可见同步的重点是在video_refresh中,下面着重分析该方法:

lastvp是上一帧,vp是当前帧,last_duration则是根据当前帧和上一帧的pts,计算出来上一帧的显示时间,经过compute_target_delay方法,计算出显示当前帧需要等待的时间。

compute_target_delay方法中,如果发现当前主时钟源不是video,则计算当前视频时钟与主时钟的差值:

  • 如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
  • 如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间
  • 如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。

回到video_refresh

frame_timer实际上就是上一帧的播放时间,而frame_timer + delay实际上就是当前这一帧的播放时间,如果系统时间还没有到当前这一帧的播放时间,直接跳转至display,而此时is->force_refresh变量为0,不显示当前帧,进入video_refresh_thread中下一次循环,并睡眠等待。

如果当前这一帧的播放时间已经过了,并且其和当前系统时间的差值超过了AV_SYNC_THRESHOLD_MAX,则将当前这一帧的播放时间改为系统时间,并在后续判断是否需要丢帧,其目的是为后面帧的播放时间重新调整frame_timer,如果缓冲区中有更多的数据,并且当前的时间已经大于当前帧的持续显示时间,则丢弃当前帧,尝试显示下一帧。

否则进入正常显示当前帧的流程,调用video_display2开始渲染。

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2017-11-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏京东技术

小程序开发效率提升神兽:京东Taro诞生记

自 2017-1-9 微信小程序(以下简称小程序)诞生以来,就伴随着赞誉与争议不断。从发布上线时的不被大多数人看好,到如今的逐渐火热,甚至说是如日中天也不为过,...

25620
来自专栏HT

HT图形组件设计之道(三)

上篇我们通过定制了CPU和内存展示界面,体验了HT for Web通过定义矢量实现图形绘制与业务数据的代码解耦及绑定联动,这类案例后续文章还会继续以便大家掌握更...

30190
来自专栏牛客网

【前端面筋】终于等到你!!!

之前一直在牛客刷面筋,今天终于自己也写了一篇,算是秋招的总结吧。希望大家都能顺利拿到自己想要的offer! lz本科妹子,从没有想过要当程序员......无奈非...

426130
来自专栏顶级程序员

Java 9、10、11,谁才是Java程序员的本命?

之前,我们在《Java 10无跳票发布,主推的新特性引争议》的文章中做了一个小的调查,主要是调查现在的Java程序员都在使用哪个版本的Java?根据调查结果,绝...

10230
来自专栏Golang语言社区

Go 的垃圾回收机制在实践中有哪些需要注意的地方?

之前回答问题的时候Go还处在1.1版本,到了1.2和1.3,Go的GC跟踪命令和GC内部实现已经有一些变化,并且根据评论中的反馈,这边一并做补充说明。 Go ...

53360
来自专栏Crossin的编程教室

【每周一坑】特殊的生日

好吧,我在跳票的道路上又双叒叕前进了一步……今天终于厚着脸皮来更新【每“周”一坑】啦。感谢在后台孜孜不倦催促我的同学们。 在出题前,先插两句: 上次关于 Pyt...

357110
来自专栏数据派THU

放弃Python转向Go语言:这9大理由就够了 !(附代码)

来源:机器之心 作者:Thierry Schellenbach 本文为你介绍八个短时间可以完成的趣味机器学习项目。 转用一门新语言通常是一项大决策,尤其是当你的...

635120
来自专栏BeJavaGod

插件推荐 - twitter分布式主键id生成器与SID

推荐一个插件,那就是idworker,用了一年了,还是挺好用,先来说说干嘛的吧,鉴于现在主键的生成模式先来探讨一下 1、id自增:比较普遍,但是在数据备份恢复的...

44660
来自专栏从零开始学自动化测试

新手学习selenium路线图(老司机亲手绘制)

前言: 最近群里有不少小白,想入手selenium,但是一直没找到学习路线,还没入门就迷路了,于是小编亲手绘制了一幅学习路线图。希望能帮助小白快速入门,帮助已经...

40760
来自专栏hightopo

HT图形组件设计之道(三)

14830

扫码关注云+社区

领取腾讯云代金券